From 376c790e1d3feb5facc6817c383b296378d4f3cd Mon Sep 17 00:00:00 2001 From: Noam Date: Fri, 30 Nov 2018 20:39:05 +0200 Subject: [PATCH 01/51] implement find multiple matches in line. start search from current selection. add unit tests. --- src/addons/search/Interfaces.ts | 7 ++- src/addons/search/SearchHelper.ts | 86 +++++++++++++++++++------------ src/addons/search/search.test.ts | 70 +++++++++++++++++++++++-- 3 files changed, 126 insertions(+), 37 deletions(-) diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index af06c5d1..e03b70c4 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -25,10 +25,13 @@ export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; + reverseSearch?: boolean; } -export interface ISearchResult { - term: string; +export interface ISearchIndex { col: number; row: number; } +export interface ISearchResult extends ISearchIndex { + term: string; +} diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 7919932a..dd055991 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces'; +import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; /** @@ -29,27 +29,30 @@ export class SearchHelper implements ISearchHelper { } let result: ISearchResult; - let startRow = this._terminal._core.buffer.ydisp; + let startCol: number = 0; if (this._terminal._core.selectionManager.selectionEnd) { // Start from the selection end if there is a selection if (this._terminal.getSelection().length !== 0) { startRow = this._terminal._core.selectionManager.selectionEnd[1]; + startCol = this._terminal._core.selectionManager.selectionEnd[0]; } } // Search from ydisp + 1 to end - for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - result = this._findInLine(term, y, searchOptions); + for (let y = startRow; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + result = this._findInLine(term, {row: y, col: startCol}, searchOptions); if (result) { break; } + startCol = 0; } // Search from the top to the current ydisp if (!result) { for (let y = 0; y < startRow; y++) { - result = this._findInLine(term, y, searchOptions); + startCol = 0; + result = this._findInLine(term, {row: y, col: startCol}, searchOptions); if (result) { break; } @@ -72,28 +75,35 @@ export class SearchHelper implements ISearchHelper { return false; } - let result: ISearchResult; + searchOptions.reverseSearch = true; + let result: ISearchResult; let startRow = this._terminal._core.buffer.ydisp; + let startCol: number = this._terminal._core.buffer.lines.get(startRow).length; + if (this._terminal._core.selectionManager.selectionStart) { // Start from the selection end if there is a selection if (this._terminal.getSelection().length !== 0) { startRow = this._terminal._core.selectionManager.selectionStart[1]; + startCol = this._terminal._core.selectionManager.selectionStart[0]; } } // Search from ydisp + 1 to end - for (let y = startRow - 1; y >= 0; y--) { - result = this._findInLine(term, y, searchOptions); + for (let y = startRow; y >= 0; y--) { + result = this._findInLine(term, {row: y, col: startCol}, searchOptions); if (result) { break; } + startCol = y > 0 ? this._terminal._core.buffer.lines.get(y - 1).length : 0; } // Search from the top to the current ydisp if (!result) { - for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) { - result = this._findInLine(term, y, searchOptions); + const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; + for (let y = searchFrom; y > startRow; y--) { + startCol = this._terminal._core.buffer.lines.get(y).length; + result = this._findInLine(term, {row: y, col: startCol}, searchOptions); if (result) { break; } @@ -125,61 +135,73 @@ export class SearchHelper implements ISearchHelper { * @param searchOptions Search options. * @return The search result if it was found. */ - protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult { - if (this._terminal._core.buffer.lines.get(y).isWrapped) { + protected _findInLine(term: string, searchIndex: ISearchIndex, searchOptions: ISearchOptions = {}): ISearchResult { + if (this._terminal._core.buffer.lines.get(searchIndex.row).isWrapped) { return; } - const stringLine = this.translateBufferLineToStringWithWrap(y, true); - const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); + const stringLine = this.translateBufferLineToStringWithWrap(searchIndex.row, true); const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); - let searchIndex = -1; + const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); + let resultIndex = -1; if (searchOptions.regex) { const searchRegex = RegExp(searchTerm, 'g'); - const foundTerm = searchRegex.exec(searchStringLine); - if (foundTerm && foundTerm[0].length > 0) { - searchIndex = searchRegex.lastIndex - foundTerm[0].length; - term = foundTerm[0]; + let foundTerm: RegExpExecArray; + if (searchOptions.reverseSearch) { + while (foundTerm = searchRegex.exec(searchStringLine.slice(0, searchIndex.col))) { + resultIndex = searchRegex.lastIndex - foundTerm[0].length; + term = foundTerm[0]; + searchRegex.lastIndex -= (term.length - 1); + } + } else { + foundTerm = searchRegex.exec(searchStringLine.slice(searchIndex.col)); + if (foundTerm && foundTerm[0].length > 0) { + resultIndex = searchIndex.col + (searchRegex.lastIndex - foundTerm[0].length); + term = foundTerm[0]; + } } } else { - searchIndex = searchStringLine.indexOf(searchTerm); + if (searchOptions.reverseSearch) { + resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length); + } else { + resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col); + } } - if (searchIndex >= 0) { + if (resultIndex >= 0) { // Adjust the row number and search index if needed since a "line" of text can span multiple rows - if (searchIndex >= this._terminal.cols) { - y += Math.floor(searchIndex / this._terminal.cols); - searchIndex = searchIndex % this._terminal.cols; + if (resultIndex >= this._terminal.cols) { + searchIndex.row += Math.floor(resultIndex / this._terminal.cols); + resultIndex = resultIndex % this._terminal.cols; } - if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) { + if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) { return; } - const line = this._terminal._core.buffer.lines.get(y); + const line = this._terminal._core.buffer.lines.get(searchIndex.row); - for (let i = 0; i < searchIndex; i++) { + for (let i = 0; i < resultIndex; i++) { const charData = line.get(i); // Adjust the searchIndex to normalize emoji into single chars const char = charData[1/*CHAR_DATA_CHAR_INDEX*/]; if (char.length > 1) { - searchIndex -= char.length - 1; + resultIndex -= char.length - 1; } // Adjust the searchIndex for empty characters following wide unicode // chars (eg. CJK) const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/]; if (charWidth === 0) { - searchIndex++; + resultIndex++; } } return { term, - col: searchIndex, - row: y + col: resultIndex, + row: searchIndex.row }; } } - /** * Translates a buffer line to a string, including subsequent lines if they are wraps. * Wide characters will count as two columns in the resulting string. This diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 3e0b8154..be9ec479 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -7,7 +7,7 @@ declare var require: any; import { assert, expect } from 'chai'; import * as search from './search'; import { SearchHelper } from './SearchHelper'; -import { ISearchOptions, ISearchResult } from './Interfaces'; +import { ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; class MockTerminalPlain {} @@ -29,8 +29,11 @@ class MockTerminal { } class TestSearchHelper extends SearchHelper { - public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult { - return this._findInLine(term, y, searchOptions); + public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult { + return this._findInLine(term, {row: rowNumber, col: 0}, searchOptions); + } + public findFromIndex(term: string, searchIndex: ISearchIndex, searchOptions?: ISearchOptions): ISearchResult { + return this._findInLine(term, searchIndex, searchOptions); } } @@ -247,5 +250,66 @@ describe('search addon', () => { expect(hello4).eql(undefined); expect(hello5).eql(undefined); }); + it('should find multiple matches in line', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 5}); + term.core.write('helloooo helloooo\r\naaaAAaaAAA'); + term.pushWriteData(); + const searchOptions = { + regex: false, + wholeWord: false, + caseSensitive: false + }; + const find0 = term.searchHelper.findFromIndex('hello', {row: 0, col: 0}, searchOptions); + const find1 = term.searchHelper.findFromIndex('hello', {row: 0, col: find0.col + find0.term.length}, searchOptions); + const find2 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: 0}, searchOptions); + const find3 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: find2.col + find2.term.length}, searchOptions); + const find4 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: find3.col + find3.term.length}, searchOptions); + expect(find0).eql({col: 0, row: 0, term: 'hello'}); + expect(find1).eql({col: 9, row: 0, term: 'hello'}); + expect(find2).eql({col: 0, row: 1, term: 'aaaa'}); + expect(find3).eql({col: 4, row: 1, term: 'aaaa'}); + expect(find4).eql(undefined); + }); + it('should find multiple matches in line - reverse search', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 5}); + term.core.write('it is what it is'); + term.pushWriteData(); + const searchOptions = { + regex: false, + wholeWord: false, + caseSensitive: false, + reverseSearch: true + }; + const find0 = term.searchHelper.findFromIndex('is', {row: 0, col: 16}, searchOptions); + const find1 = term.searchHelper.findFromIndex('is', {row: 0, col: find0.col}, searchOptions); + const find2 = term.searchHelper.findFromIndex('it', {row: 0, col: 16}, searchOptions); + const find3 = term.searchHelper.findFromIndex('it', {row: 0, col: find2.col}, searchOptions); + expect(find0).eql({col: 14, row: 0, term: 'is'}); + expect(find1).eql({col: 3, row: 0, term: 'is'}); + expect(find2).eql({col: 11, row: 0, term: 'it'}); + expect(find3).eql({col: 0, row: 0, term: 'it'}); + }); + it('should find multiple matches in line - reverse search with regex', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 5}); + term.core.write('zzzABCzzzzABCABC'); + term.pushWriteData(); + const searchOptions = { + regex: true, + wholeWord: false, + caseSensitive: true, + reverseSearch: true + }; + const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: 16}, searchOptions); + const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find0.col}, searchOptions); + const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find1.col}, searchOptions); + const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find2.col}, searchOptions); + expect(find0).eql({col: 13, row: 0, term: 'ABC'}); + expect(find1).eql({col: 10, row: 0, term: 'ABC'}); + expect(find2).eql({col: 3, row: 0, term: 'ABC'}); + expect(find3).eql(undefined); + }); }); }); From 4d660deff3db9f3eea3923dcaa970fde0e9deaf8 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 4 Dec 2018 19:00:44 -0800 Subject: [PATCH 02/51] hooks for custom control sequences This fixes (at least partially) issue #1176 "Add a way to plugin a custom control sequence handler". --- src/EscapeSequenceParser.ts | 78 ++++++++++++++++++++++++++++++++----- src/InputHandler.ts | 17 +++++++- src/Terminal.ts | 6 ++- src/Types.ts | 14 +++++++ src/public/Terminal.ts | 5 ++- 5 files changed, 106 insertions(+), 14 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index b38c50f5..7a5da2c8 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -301,9 +301,39 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } - setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { - this._csiHandlers[flag.charCodeAt(0)] = callback; + private _removeHandler(array: any[], callback: any): void { + if (array) { + for (let i = array.length; --i >= 0; ) { + if (array[i] == callback) { + array.splice(i, 1); + return; + } + } + } } + + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { + let index = flag.charCodeAt(0); + let array = this._csiHandlers[index]; + if (! array) { this._csiHandlers[index] = array = new Array(); } + array.push(callback); + } + + removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { + let index = flag.charCodeAt(0); + let array = this._csiHandlers[index]; + this._removeHandler(array, callback); + if (array && array.length == 0) + delete this._csiHandlers[index]; + } + /* deprecated */ + setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { + this.clearCsiHandler(flag); + this.addCsiHandler(flag, (params: number[], collect: string): boolean => { + callback(params, collect); return true; + }); + } + /* deprecated */ clearCsiHandler(flag: string): void { if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; } @@ -321,9 +351,25 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlerFb = callback; } - setOscHandler(ident: number, callback: (data: string) => void): void { - this._oscHandlers[ident] = callback; + addOscHandler(ident: number, callback: (data: string) => boolean): void { + let array = this._oscHandlers[ident]; + if (! array) { this._oscHandlers[ident] = array = new Array(); } + array.push(callback); } + removeOscHandler(ident: number, callback: (data: string) => boolean): void { + let array = this._oscHandlers[ident]; + this._removeHandler(array, callback); + if (array && array.length == 0) + delete this._oscHandlers[ident]; + } + /* deprecated */ + setOscHandler(ident: number, callback: (data: string) => void): void { + this.clearOscHandler(ident); + this.addOscHandler(ident, (data: string): boolean => { + callback(data); return true; + }); + } + /* deprecated */ clearOscHandler(ident: number): void { if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; } @@ -463,9 +509,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.CSI_DISPATCH: - callback = this._csiHandlers[code]; - if (callback) callback(params, collect); - else this._csiHandlerFb(collect, params, code); + let cHandler = this._csiHandlers[code]; + if (cHandler) { + for (let i = cHandler.length; ;) { + if (--i < 0) { cHandler = null; break; } + if ((cHandler[i])(params, collect)) + break; + } + } + if (! cHandler) this._csiHandlerFb(collect, params, code); break; case ParserAction.PARAM: if (code === 0x3b) params.push(0); @@ -532,9 +584,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // or with an explicit NaN OSC handler const identifier = parseInt(osc.substring(0, idx)); const content = osc.substring(idx + 1); - callback = this._oscHandlers[identifier]; - if (callback) callback(content); - else this._oscHandlerFb(identifier, content); + let oHandler = this._oscHandlers[identifier]; + if (oHandler) { + for (let i = oHandler.length; ;) { + if (--i < 0) { oHandler = null; break; } + if ((oHandler[i])(content)) + break; + } + } + if (! oHandler) this._oscHandlerFb(identifier, content); } } if (code === 0x1b) transition |= ParserState.ESCAPE; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7604b01f..2c931a2b 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IVtInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; @@ -112,7 +112,7 @@ class DECRQSS implements IDcsHandler { * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand * each function's header comment. */ -export class InputHandler extends Disposable implements IInputHandler { +export class InputHandler extends Disposable implements IVtInputHandler { private _surrogateFirst: string; constructor( @@ -465,6 +465,19 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.y); } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { + this._parser.addCsiHandler(flag, callback); + } + removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { + this._parser.removeCsiHandler(flag, callback); + } + addOscHandler(ident: number, callback: (data: string) => boolean): void { + this._parser.setOscHandler(ident, callback); + } + removeOscHandler(ident: number, callback: (data: string) => boolean): void { + this._parser.removeOscHandler(ident, callback); + } + /** * BEL * Bell (Ctrl-G). diff --git a/src/Terminal.ts b/src/Terminal.ts index 2cfc1ca8..fea0cb69 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; +import { IInputHandlingTerminal, IInputHandler, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -1286,6 +1286,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.refresh(0, this.rows - 1); } + public get inputHandler(): IInputHandler { + return this._inputHandler; + } + /** * Scroll the display of the terminal by a number of pages. * @param pageCount The number of pages to scroll (negative scrolls up). diff --git a/src/Types.ts b/src/Types.ts index 430c6575..93dd7c8a 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -182,6 +182,16 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; } +/* + * An InputHandler for VT-style terminals + */ +export interface IVtInputHandler extends IInputHandler { + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; + removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; + addOscHandler(ident: number, callback: (data: string) => boolean): void; + removeOscHandler(ident: number, callback: (data: string) => boolean): void; +} + export interface ILinkMatcher { id: number; regex: RegExp; @@ -492,6 +502,10 @@ export interface IEscapeSequenceParser extends IDisposable { setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; clearCsiHandler(flag: string): void; setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; + removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; + addOscHandler(ident: number, callback: (data: string) => boolean): void; + removeOscHandler(ident: number, callback: (data: string) => boolean): void; setEscHandler(collectAndFlag: string, callback: () => void): void; clearEscHandler(collectAndFlag: string): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 8ff7cf2b..de15fad0 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -4,7 +4,7 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm'; -import { ITerminal } from '../Types'; +import { ITerminal, IInputHandler } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; @@ -15,6 +15,9 @@ export class Terminal implements ITerminalApi { this._core = new TerminalCore(options); } + public get inputHandler(): IInputHandler { + return (this._core as TerminalCore).inputHandler; + } public get element(): HTMLElement { return this._core.element; } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } From b68974517e2f9f02b0304db064020e25afc527fd Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 5 Dec 2018 14:45:57 -0800 Subject: [PATCH 03/51] Cleanups required by tslink. --- src/EscapeSequenceParser.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 7a5da2c8..5a2c6cd3 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -304,7 +304,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP private _removeHandler(array: any[], callback: any): void { if (array) { for (let i = array.length; --i >= 0; ) { - if (array[i] == callback) { + if (array[i] === callback) { array.splice(i, 1); return; } @@ -313,18 +313,19 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - let index = flag.charCodeAt(0); + const index = flag.charCodeAt(0); let array = this._csiHandlers[index]; if (! array) { this._csiHandlers[index] = array = new Array(); } array.push(callback); } removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - let index = flag.charCodeAt(0); - let array = this._csiHandlers[index]; + const index = flag.charCodeAt(0); + const array = this._csiHandlers[index]; this._removeHandler(array, callback); - if (array && array.length == 0) + if (array && array.length === 0) { delete this._csiHandlers[index]; + } } /* deprecated */ setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { @@ -357,10 +358,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP array.push(callback); } removeOscHandler(ident: number, callback: (data: string) => boolean): void { - let array = this._oscHandlers[ident]; + const array = this._oscHandlers[ident]; this._removeHandler(array, callback); - if (array && array.length == 0) + if (array && array.length === 0) { delete this._oscHandlers[ident]; + } } /* deprecated */ setOscHandler(ident: number, callback: (data: string) => void): void { @@ -513,8 +515,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (cHandler) { for (let i = cHandler.length; ;) { if (--i < 0) { cHandler = null; break; } - if ((cHandler[i])(params, collect)) - break; + if ((cHandler[i])(params, collect)) { break; } } } if (! cHandler) this._csiHandlerFb(collect, params, code); @@ -588,8 +589,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (oHandler) { for (let i = oHandler.length; ;) { if (--i < 0) { oHandler = null; break; } - if ((oHandler[i])(content)) - break; + if ((oHandler[i])(content)) { break; } } } if (! oHandler) this._oscHandlerFb(identifier, content); From 278d696709b799c20481107ebf2a6ff53ae0764b Mon Sep 17 00:00:00 2001 From: Noam Date: Fri, 7 Dec 2018 22:56:57 +0200 Subject: [PATCH 04/51] remove reverseSearch from ISearchOptions add isReverseSearch argument to findInLine modify unit tests --- src/addons/search/Interfaces.ts | 2 +- src/addons/search/SearchHelper.ts | 15 ++++++--------- src/addons/search/search.test.ts | 28 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index e03b70c4..e15224d1 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -25,13 +25,13 @@ export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; - reverseSearch?: boolean; } export interface ISearchIndex { col: number; row: number; } + export interface ISearchResult extends ISearchIndex { term: string; } diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index dd055991..5052a07d 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -5,7 +5,6 @@ import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; - /** * A class that knows how to search the terminal and how to display the results. */ @@ -74,9 +73,7 @@ export class SearchHelper implements ISearchHelper { if (!term || term.length === 0) { return false; } - - searchOptions.reverseSearch = true; - + const isReverseSearch = true; let result: ISearchResult; let startRow = this._terminal._core.buffer.ydisp; let startCol: number = this._terminal._core.buffer.lines.get(startRow).length; @@ -91,7 +88,7 @@ export class SearchHelper implements ISearchHelper { // Search from ydisp + 1 to end for (let y = startRow; y >= 0; y--) { - result = this._findInLine(term, {row: y, col: startCol}, searchOptions); + result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); if (result) { break; } @@ -103,7 +100,7 @@ export class SearchHelper implements ISearchHelper { const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; for (let y = searchFrom; y > startRow; y--) { startCol = this._terminal._core.buffer.lines.get(y).length; - result = this._findInLine(term, {row: y, col: startCol}, searchOptions); + result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); if (result) { break; } @@ -135,7 +132,7 @@ export class SearchHelper implements ISearchHelper { * @param searchOptions Search options. * @return The search result if it was found. */ - protected _findInLine(term: string, searchIndex: ISearchIndex, searchOptions: ISearchOptions = {}): ISearchResult { + protected _findInLine(term: string, searchIndex: ISearchIndex, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { if (this._terminal._core.buffer.lines.get(searchIndex.row).isWrapped) { return; } @@ -148,7 +145,7 @@ export class SearchHelper implements ISearchHelper { if (searchOptions.regex) { const searchRegex = RegExp(searchTerm, 'g'); let foundTerm: RegExpExecArray; - if (searchOptions.reverseSearch) { + if (isReverseSearch) { while (foundTerm = searchRegex.exec(searchStringLine.slice(0, searchIndex.col))) { resultIndex = searchRegex.lastIndex - foundTerm[0].length; term = foundTerm[0]; @@ -162,7 +159,7 @@ export class SearchHelper implements ISearchHelper { } } } else { - if (searchOptions.reverseSearch) { + if (isReverseSearch) { resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length); } else { resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col); diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index be9ec479..0a38f755 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -32,8 +32,8 @@ class TestSearchHelper extends SearchHelper { public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult { return this._findInLine(term, {row: rowNumber, col: 0}, searchOptions); } - public findFromIndex(term: string, searchIndex: ISearchIndex, searchOptions?: ISearchOptions): ISearchResult { - return this._findInLine(term, searchIndex, searchOptions); + public findFromIndex(term: string, searchIndex: ISearchIndex, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult { + return this._findInLine(term, searchIndex, searchOptions, isReverseSearch); } } @@ -279,13 +279,13 @@ describe('search addon', () => { const searchOptions = { regex: false, wholeWord: false, - caseSensitive: false, - reverseSearch: true + caseSensitive: false }; - const find0 = term.searchHelper.findFromIndex('is', {row: 0, col: 16}, searchOptions); - const find1 = term.searchHelper.findFromIndex('is', {row: 0, col: find0.col}, searchOptions); - const find2 = term.searchHelper.findFromIndex('it', {row: 0, col: 16}, searchOptions); - const find3 = term.searchHelper.findFromIndex('it', {row: 0, col: find2.col}, searchOptions); + const isReverseSearch = true; + const find0 = term.searchHelper.findFromIndex('is', {row: 0, col: 16}, searchOptions, isReverseSearch); + const find1 = term.searchHelper.findFromIndex('is', {row: 0, col: find0.col}, searchOptions, isReverseSearch); + const find2 = term.searchHelper.findFromIndex('it', {row: 0, col: 16}, searchOptions, isReverseSearch); + const find3 = term.searchHelper.findFromIndex('it', {row: 0, col: find2.col}, searchOptions, isReverseSearch); expect(find0).eql({col: 14, row: 0, term: 'is'}); expect(find1).eql({col: 3, row: 0, term: 'is'}); expect(find2).eql({col: 11, row: 0, term: 'it'}); @@ -299,13 +299,13 @@ describe('search addon', () => { const searchOptions = { regex: true, wholeWord: false, - caseSensitive: true, - reverseSearch: true + caseSensitive: true }; - const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: 16}, searchOptions); - const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find0.col}, searchOptions); - const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find1.col}, searchOptions); - const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find2.col}, searchOptions); + const isReverseSearch = true; + const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: 16}, searchOptions, isReverseSearch); + const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find0.col}, searchOptions, isReverseSearch); + const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find1.col}, searchOptions, isReverseSearch); + const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find2.col}, searchOptions, isReverseSearch); expect(find0).eql({col: 13, row: 0, term: 'ABC'}); expect(find1).eql({col: 10, row: 0, term: 'ABC'}); expect(find2).eql({col: 3, row: 0, term: 'ABC'}); From 03115639a959c19ad9a5b6e112cab2066cb46c7f Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 9 Dec 2018 10:56:34 -0800 Subject: [PATCH 05/51] Optimize parsing of OSC_STRING to minimize string concatenation. --- src/EscapeSequenceParser.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 5a2c6cd3..28a946d1 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -439,7 +439,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } // normal transition & action lookup - transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION; + transition = (code < 0xa0 + ? (table[currentState << 8 | code]) + : currentState === ParserState.OSC_STRING + ? (ParserAction.OSC_PUT << 4) | ParserState.OSC_STRING + : DEFAULT_TRANSITION); switch (transition >> 4) { case ParserAction.PRINT: print = (~print) ? print : i; @@ -471,10 +475,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserState.GROUND: print = (~print) ? print : i; break; - case ParserState.OSC_STRING: - osc += String.fromCharCode(code); - transition |= ParserState.OSC_STRING; - break; case ParserState.CSI_IGNORE: transition |= ParserState.CSI_IGNORE; break; @@ -570,7 +570,16 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc = ''; break; case ParserAction.OSC_PUT: - osc += data.charAt(i); + for (let j = i + 1; ; j++) { + if (j >= l + || ((code = data.charCodeAt(j)) <= 0x9f + && (table[ParserState.OSC_STRING << 8 | code] >> 4 + !== ParserAction.OSC_PUT))) { + osc += data.substring(i, j); + i = j - 1; + break; + } + } break; case ParserAction.OSC_END: if (osc && code !== 0x18 && code !== 0x1a) { From 30a667c3be32cbccc57405b58a787a41668491e4 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 9 Dec 2018 13:11:17 -0800 Subject: [PATCH 06/51] Revert "Optimize parsing of OSC_STRING to minimize string concatenation." This reverts commit 03115639a959c19ad9a5b6e112cab2066cb46c7f. --- src/EscapeSequenceParser.ts | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 28a946d1..5a2c6cd3 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -439,11 +439,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } // normal transition & action lookup - transition = (code < 0xa0 - ? (table[currentState << 8 | code]) - : currentState === ParserState.OSC_STRING - ? (ParserAction.OSC_PUT << 4) | ParserState.OSC_STRING - : DEFAULT_TRANSITION); + transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION; switch (transition >> 4) { case ParserAction.PRINT: print = (~print) ? print : i; @@ -475,6 +471,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserState.GROUND: print = (~print) ? print : i; break; + case ParserState.OSC_STRING: + osc += String.fromCharCode(code); + transition |= ParserState.OSC_STRING; + break; case ParserState.CSI_IGNORE: transition |= ParserState.CSI_IGNORE; break; @@ -570,16 +570,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc = ''; break; case ParserAction.OSC_PUT: - for (let j = i + 1; ; j++) { - if (j >= l - || ((code = data.charCodeAt(j)) <= 0x9f - && (table[ParserState.OSC_STRING << 8 | code] >> 4 - !== ParserAction.OSC_PUT))) { - osc += data.substring(i, j); - i = j - 1; - break; - } - } + osc += data.charAt(i); break; case ParserAction.OSC_END: if (osc && code !== 0x18 && code !== 0x1a) { From 91ef7f24a60d6ac59c7a425fe7e72a5dc5624ea6 Mon Sep 17 00:00:00 2001 From: Noj Vek Date: Mon, 10 Dec 2018 15:54:35 -0800 Subject: [PATCH 07/51] Fixes #1660: Search as you type --- demo/client.ts | 38 ++++++++++++++---------------- src/addons/search/Interfaces.ts | 2 ++ src/addons/search/SearchHelper.ts | 39 +++++++++++++++++++------------ 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index d5196d37..7fd2a894 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -14,6 +14,7 @@ import * as fullscreen from '../lib/addons/fullscreen/fullscreen'; import * as search from '../lib/addons/search/search'; import * as webLinks from '../lib/addons/webLinks/webLinks'; import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat'; +import { ISearchOptions } from '../lib/addons/search/Interfaces'; // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module @@ -50,6 +51,14 @@ function setPadding(): void { term.fit(); } +function getSearchOptions(): ISearchOptions { + return { + regex: (document.getElementById('regex') as HTMLInputElement).checked, + wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, + caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked, + }; +} + createTerminal(); const disposeRecreateButtonHandler = () => { @@ -97,27 +106,16 @@ function createTerminal(): void { addDomListener(paddingElement, 'change', setPadding); - addDomListener(actionElements.findNext, 'keypress', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const searchOptions = { - regex: (document.getElementById('regex') as HTMLInputElement).checked, - wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, - caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked - }; - term.findNext(actionElements.findNext.value, searchOptions); - } + addDomListener(actionElements.findNext, 'keyup', (e) => { + const searchOptions = getSearchOptions(); + searchOptions.incremental = e.key !== `Enter`; + term.findNext(actionElements.findNext.value, searchOptions); }); - addDomListener(actionElements.findPrevious, 'keypress', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const searchOptions = { - regex: (document.getElementById('regex') as HTMLInputElement).checked, - wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, - caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked - }; - term.findPrevious(actionElements.findPrevious.value, searchOptions); - } + + addDomListener(actionElements.findPrevious, 'keyup', (e) => { + const searchOptions = getSearchOptions(); + searchOptions.incremental = e.key !== `Enter`; + term.findPrevious(actionElements.findPrevious.value, searchOptions); }); // fit is called within a setTimeout, cols and rows need this. diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index af06c5d1..96788120 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -25,6 +25,8 @@ export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; + /** Assume caller implements 'search as you type' where findNext gets called when search input changes */ + incremental?: boolean; } export interface ISearchResult { diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 7919932a..d5ec0b48 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -24,29 +24,34 @@ export class SearchHelper implements ISearchHelper { * @return Whether a result was found. */ public findNext(term: string, searchOptions?: ISearchOptions): boolean { + const selectionManager = this._terminal._core.selectionManager; + const {incremental} = searchOptions; + let result: ISearchResult; + if (!term || term.length === 0) { + selectionManager.clearSelection(); return false; } - let result: ISearchResult; - let startRow = this._terminal._core.buffer.ydisp; - if (this._terminal._core.selectionManager.selectionEnd) { + + if (selectionManager.selectionEnd) { // Start from the selection end if there is a selection + // For incremental search, use existing row if (this._terminal.getSelection().length !== 0) { - startRow = this._terminal._core.selectionManager.selectionEnd[1]; + startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1]; } } - // Search from ydisp + 1 to end - for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + // Search from startRow to end + for (let y = incremental ? startRow: startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { result = this._findInLine(term, y, searchOptions); if (result) { break; } } - // Search from the top to the current ydisp + // Search from the top to the startRow if (!result) { for (let y = 0; y < startRow; y++) { result = this._findInLine(term, y, searchOptions); @@ -68,29 +73,33 @@ export class SearchHelper implements ISearchHelper { * @return Whether a result was found. */ public findPrevious(term: string, searchOptions?: ISearchOptions): boolean { + const selectionManager = this._terminal._core.selectionManager; + const {incremental} = searchOptions; + let result: ISearchResult; + if (!term || term.length === 0) { + selectionManager.clearSelection(); return false; } - let result: ISearchResult; - let startRow = this._terminal._core.buffer.ydisp; - if (this._terminal._core.selectionManager.selectionStart) { - // Start from the selection end if there is a selection + + if (selectionManager.selectionStart) { + // Start from the selection start if there is a selection if (this._terminal.getSelection().length !== 0) { - startRow = this._terminal._core.selectionManager.selectionStart[1]; + startRow = selectionManager.selectionStart[1]; } } - // Search from ydisp + 1 to end - for (let y = startRow - 1; y >= 0; y--) { + // Search from startRow to top + for (let y = incremental ? startRow : startRow - 1; y >= 0; y--) { result = this._findInLine(term, y, searchOptions); if (result) { break; } } - // Search from the top to the current ydisp + // Search from the bottom to startRow if (!result) { for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) { result = this._findInLine(term, y, searchOptions); From 9005de1c42cc4cc3684aa87e7222efee4209b5d5 Mon Sep 17 00:00:00 2001 From: Noj Vek Date: Mon, 10 Dec 2018 17:13:28 -0800 Subject: [PATCH 08/51] adding a linesCache ttl and cursor move invalidation --- demo/client.ts | 2 +- src/addons/search/SearchHelper.ts | 55 ++++++++++++++++++++++++++----- src/addons/search/tsconfig.json | 1 + 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 700fa38d..d99ff66d 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -55,7 +55,7 @@ function getSearchOptions(): ISearchOptions { return { regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, - caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked, + caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked }; } diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index d5ec0b48..cfb29c8b 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -4,16 +4,24 @@ */ import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces'; -const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; + +const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; +const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs /** * A class that knows how to search the terminal and how to display the results. */ export class SearchHelper implements ISearchHelper { + /** + * translateBufferLineToStringWithWrap is a fairly expensive call. + * We memoize the calls into an array that has a time based ttl. + * _linesCache is also invalidated when the terminal cursor moves. + */ + private _linesCache: string[] = null; + private _linesCacheTimeoutId = 0; + constructor(private _terminal: ISearchAddonTerminal) { - // TODO: Search for multiple instances on 1 line - // TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted - // TODO: Highlight other instances in the viewport + this._destroyLinesCache = this._destroyLinesCache.bind(this); } /** @@ -43,8 +51,10 @@ export class SearchHelper implements ISearchHelper { } } + this._initLinesCache(); + // Search from startRow to end - for (let y = incremental ? startRow: startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + for (let y = incremental ? startRow : startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { result = this._findInLine(term, y, searchOptions); if (result) { break; @@ -91,6 +101,8 @@ export class SearchHelper implements ISearchHelper { } } + this._initLinesCache(); + // Search from startRow to top for (let y = incremental ? startRow : startRow - 1; y >= 0; y--) { result = this._findInLine(term, y, searchOptions); @@ -113,6 +125,28 @@ export class SearchHelper implements ISearchHelper { return this._selectResult(result); } + /** + * Sets up a line cache with a ttl + */ + private _initLinesCache(): void { + if (!this._linesCache) { + this._linesCache = new Array(this._terminal._core.buffer.length); + this._terminal.on('cursormove', this._destroyLinesCache); + } + + window.clearTimeout(this._linesCacheTimeoutId); + this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE); + } + + private _destroyLinesCache(): void { + this._linesCache = null; + this._terminal.off('cursormove', this._destroyLinesCache); + if (this._linesCacheTimeoutId) { + window.clearTimeout(this._linesCacheTimeoutId); + this._linesCacheTimeoutId = 0; + } + } + /** * A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it. * @param searchIndex starting indext of the potential whole word substring @@ -120,8 +154,8 @@ export class SearchHelper implements ISearchHelper { * @param term the substring that starts at searchIndex */ private _isWholeWord(searchIndex: number, line: string, term: string): boolean { - return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) && - (((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1))); + return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) && + (((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1))); } /** @@ -139,7 +173,12 @@ export class SearchHelper implements ISearchHelper { return; } - const stringLine = this.translateBufferLineToStringWithWrap(y, true); + let stringLine = this._linesCache[y]; + if (stringLine === void 0) { + stringLine = this.translateBufferLineToStringWithWrap(y, true); + this._linesCache[y] = stringLine; + } + const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); let searchIndex = -1; diff --git a/src/addons/search/tsconfig.json b/src/addons/search/tsconfig.json index c34a0bc5..e7a1ff3d 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -3,6 +3,7 @@ "module": "commonjs", "target": "es5", "lib": [ + "dom", "es5" ], "rootDir": ".", From a0d583b075e454ef9f2f2fe3d4c08ed4c47f664c Mon Sep 17 00:00:00 2001 From: Noj Vek Date: Mon, 10 Dec 2018 17:23:28 -0800 Subject: [PATCH 09/51] handle non-cached scenario in tests land --- src/addons/search/SearchHelper.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index cfb29c8b..8e6e4e12 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -173,10 +173,12 @@ export class SearchHelper implements ISearchHelper { return; } - let stringLine = this._linesCache[y]; + let stringLine = this._linesCache ? this._linesCache[y] : void 0; if (stringLine === void 0) { stringLine = this.translateBufferLineToStringWithWrap(y, true); - this._linesCache[y] = stringLine; + if (this._linesCache) { + this._linesCache[y] = stringLine; + } } const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); From a0a8003d608d180f551f3745f2fe1f747e35dc98 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Dec 2018 09:06:50 -0800 Subject: [PATCH 10/51] Add unit tests debug target --- .gitignore | 1 - .vscode/launch.json | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index 25e93d9b..b50ab2d9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ npm-debug.log /.idea/ .env build/ -.vscode/ .DS_Store fixtures/typings-test/*.js package-lock.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..1c3aaf0f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Unit Tests", + "cwd": "${workspaceRoot}", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha", + "windows": { + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd" + }, + "runtimeArgs": [ + "--colors", + "--recursive", + "${workspaceRoot}/lib" + ], + "sourceMaps": true, + "outFiles": [ "${workspaceRoot}/lib/**/*.js" ], + "internalConsoleOptions": "openOnSessionStart" + } + ] +} From 943def1391e06f08488c574c681eb9ffd4aef474 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Dec 2018 09:37:20 -0800 Subject: [PATCH 11/51] Add debug target for client, fix source maps in demo to point to ts --- .vscode/launch.json | 10 ++++++++++ demo/start.js | 5 +++++ package.json | 2 +- yarn.lock | 21 +++++---------------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1c3aaf0f..36008195 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,6 +18,16 @@ "sourceMaps": true, "outFiles": [ "${workspaceRoot}/lib/**/*.js" ], "internalConsoleOptions": "openOnSessionStart" + }, + { + "type": "chrome", + "request": "launch", + "name": "Demo", + "url": "http://0.0.0.0:3000", + "windows": { + "url": "http://127.0.0.1:3000" + }, + "webRoot": "${workspaceFolder}/" } ] } diff --git a/demo/start.js b/demo/start.js index ad9e9f2f..f53ae7cc 100644 --- a/demo/start.js +++ b/demo/start.js @@ -22,6 +22,11 @@ const clientConfig = { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ + }, + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre" } ] }, diff --git a/package.json b/package.json index 4aef95af..d4c4099b 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "nodemon": "1.10.2", "nyc": "^11.8.0", "sorcery": "^0.10.0", - "source-map-loader": "^0.2.3", + "source-map-loader": "^0.2.4", "ts-loader": "^4.5.0", "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", diff --git a/yarn.lock b/yarn.lock index db31bfe1..54fdb7bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3894,16 +3894,6 @@ loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" -loader-utils@~0.2.2: - version "0.2.17" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" - integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - object-assign "^4.0.1" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -6039,14 +6029,13 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" integrity sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A== -source-map-loader@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.3.tgz#d4b0c8cd47d54edce3e6bfa0f523f452b5b0e521" - integrity sha512-MYbFX9DYxmTQFfy2v8FC1XZwpwHKYxg3SK8Wb7VPBKuhDjz8gi9re2819MsG4p49HDyiOSUKlmZ+nQBArW5CGw== +source-map-loader@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.4.tgz#c18b0dc6e23bf66f6792437557c569a11e072271" + integrity sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ== dependencies: async "^2.5.0" - loader-utils "~0.2.2" - source-map "~0.6.1" + loader-utils "^1.1.0" source-map-resolve@^0.5.0, source-map-resolve@^0.5.1: version "0.5.2" From d7f284c676eb59ad917d23675e8e9917648b951c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Dec 2018 09:56:33 -0800 Subject: [PATCH 12/51] Add debug target for demo server, merge 2 start processes into one --- .vscode/launch.json | 13 +++- demo/server.js | 174 +++++++++++++++++++++++--------------------- demo/start.js | 4 +- package.json | 1 + 4 files changed, 105 insertions(+), 87 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 36008195..c7bf7381 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,12 +22,23 @@ { "type": "chrome", "request": "launch", - "name": "Demo", + "name": "Demo Client", "url": "http://0.0.0.0:3000", "windows": { "url": "http://127.0.0.1:3000" }, "webRoot": "${workspaceFolder}/" + }, + { + "type": "node", + "request": "launch", + "name": "Demo Server", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "start-debug" + ], + "port": 9229 } ] } diff --git a/demo/server.js b/demo/server.js index ebe2f644..5ff9ca61 100644 --- a/demo/server.js +++ b/demo/server.js @@ -1,100 +1,106 @@ var express = require('express'); -var app = express(); -var expressWs = require('express-ws')(app); +var expressWs = require('express-ws'); var os = require('os'); var pty = require('node-pty'); -var terminals = {}, - logs = {}; +function startServer() { + var app = express(); + expressWs(app); -app.use('/build', express.static(__dirname + '/../build')); + var terminals = {}, + logs = {}; -app.get('/', function(req, res){ - res.sendFile(__dirname + '/index.html'); -}); + app.use('/build', express.static(__dirname + '/../build')); -app.get('/style.css', function(req, res){ - res.sendFile(__dirname + '/style.css'); -}); - -app.get('/dist/client-bundle.js', function(req, res){ - res.sendFile(__dirname + '/dist/client-bundle.js'); -}); - -app.post('/terminals', function (req, res) { - var cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { - name: 'xterm-color', - cols: cols || 80, - rows: rows || 24, - cwd: process.env.PWD, - env: process.env - }); - - console.log('Created terminal with PID: ' + term.pid); - terminals[term.pid] = term; - logs[term.pid] = ''; - term.on('data', function(data) { - logs[term.pid] += data; + app.get('/', function(req, res){ + res.sendFile(__dirname + '/index.html'); }); - res.send(term.pid.toString()); - res.end(); -}); -app.post('/terminals/:pid/size', function (req, res) { - var pid = parseInt(req.params.pid), - cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = terminals[pid]; + app.get('/style.css', function(req, res){ + res.sendFile(__dirname + '/style.css'); + }); - term.resize(cols, rows); - console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.'); - res.end(); -}); + app.get('/dist/client-bundle.js', function(req, res){ + res.sendFile(__dirname + '/dist/client-bundle.js'); + }); -app.ws('/terminals/:pid', function (ws, req) { - var term = terminals[parseInt(req.params.pid)]; - console.log('Connected to terminal ' + term.pid); - ws.send(logs[term.pid]); + app.post('/terminals', function (req, res) { + var cols = parseInt(req.query.cols), + rows = parseInt(req.query.rows), + term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { + name: 'xterm-color', + cols: cols || 80, + rows: rows || 24, + cwd: process.env.PWD, + env: process.env + }); - function buffer(socket, timeout) { - let s = ''; - let sender = null; - return (data) => { - s += data; - if (!sender) { - sender = setTimeout(() => { - socket.send(s); - s = ''; - sender = null; - }, timeout); - } - }; - } - const send = buffer(ws, 5); + console.log('Created terminal with PID: ' + term.pid); + terminals[term.pid] = term; + logs[term.pid] = ''; + term.on('data', function(data) { + logs[term.pid] += data; + }); + res.send(term.pid.toString()); + res.end(); + }); - term.on('data', function(data) { - try { - send(data); - } catch (ex) { - // The WebSocket is not open, ignore + app.post('/terminals/:pid/size', function (req, res) { + var pid = parseInt(req.params.pid), + cols = parseInt(req.query.cols), + rows = parseInt(req.query.rows), + term = terminals[pid]; + + term.resize(cols, rows); + console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.'); + res.end(); + }); + + app.ws('/terminals/:pid', function (ws, req) { + var term = terminals[parseInt(req.params.pid)]; + console.log('Connected to terminal ' + term.pid); + ws.send(logs[term.pid]); + + function buffer(socket, timeout) { + let s = ''; + let sender = null; + return (data) => { + s += data; + if (!sender) { + sender = setTimeout(() => { + socket.send(s); + s = ''; + sender = null; + }, timeout); + } + }; } - }); - ws.on('message', function(msg) { - term.write(msg); - }); - ws.on('close', function () { - term.kill(); - console.log('Closed terminal ' + term.pid); - // Clean things up - delete terminals[term.pid]; - delete logs[term.pid]; - }); -}); + const send = buffer(ws, 5); -var port = process.env.PORT || 3000, - host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; + term.on('data', function(data) { + try { + send(data); + } catch (ex) { + // The WebSocket is not open, ignore + } + }); + ws.on('message', function(msg) { + term.write(msg); + }); + ws.on('close', function () { + term.kill(); + console.log('Closed terminal ' + term.pid); + // Clean things up + delete terminals[term.pid]; + delete logs[term.pid]; + }); + }); -console.log('App listening to http://' + host + ':' + port); -app.listen(port, host); + var port = process.env.PORT || 3000, + host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; + + console.log('App listening to http://' + host + ':' + port); + app.listen(port, host); +} + +module.exports = startServer; diff --git a/demo/start.js b/demo/start.js index f53ae7cc..78f1ff1d 100644 --- a/demo/start.js +++ b/demo/start.js @@ -8,9 +8,9 @@ const cp = require('child_process'); const path = require('path'); const webpack = require('webpack'); +const startServer = require('./server.js'); -// Launch server -cp.spawn('node', [path.resolve(__dirname, 'server.js')], { stdio: 'inherit' }); +startServer(); // Build/watch client source const clientConfig = { diff --git a/package.json b/package.json index d4c4099b..b9d1d34d 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ }, "scripts": { "start": "node demo/start", + "start-debug": "node --inspect-brk demo/start", "start-zmodem": "node demo/zmodem/app", "lint": "tslint 'src/**/*.ts' './demo/**/*.ts'", "pretest": "npm run layering", From ffb2708a8128a0cf1637c1f57de3c35ceca6029b Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 11 Dec 2018 16:50:14 -0800 Subject: [PATCH 13/51] Revert "Cleanups required by tslink." This reverts commit b68974517e2f9f02b0304db064020e25afc527fd. --- src/EscapeSequenceParser.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 76b9dd68..90405a42 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -306,7 +306,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP private _removeHandler(array: any[], callback: any): void { if (array) { for (let i = array.length; --i >= 0; ) { - if (array[i] === callback) { + if (array[i] == callback) { array.splice(i, 1); return; } @@ -315,19 +315,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - const index = flag.charCodeAt(0); + let index = flag.charCodeAt(0); let array = this._csiHandlers[index]; if (! array) { this._csiHandlers[index] = array = new Array(); } array.push(callback); } removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - const index = flag.charCodeAt(0); - const array = this._csiHandlers[index]; + let index = flag.charCodeAt(0); + let array = this._csiHandlers[index]; this._removeHandler(array, callback); - if (array && array.length === 0) { + if (array && array.length == 0) delete this._csiHandlers[index]; - } } /* deprecated */ setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { @@ -360,11 +359,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP array.push(callback); } removeOscHandler(ident: number, callback: (data: string) => boolean): void { - const array = this._oscHandlers[ident]; + let array = this._oscHandlers[ident]; this._removeHandler(array, callback); - if (array && array.length === 0) { + if (array && array.length == 0) delete this._oscHandlers[ident]; - } } /* deprecated */ setOscHandler(ident: number, callback: (data: string) => void): void { @@ -513,7 +511,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (cHandler) { for (let i = cHandler.length; ;) { if (--i < 0) { cHandler = null; break; } - if ((cHandler[i])(params, collect)) { break; } + if ((cHandler[i])(params, collect)) + break; } } if (! cHandler) this._csiHandlerFb(collect, params, code); @@ -595,7 +594,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (oHandler) { for (let i = oHandler.length; ;) { if (--i < 0) { oHandler = null; break; } - if ((oHandler[i])(content)) { break; } + if ((oHandler[i])(content)) + break; } } if (! oHandler) this._oscHandlerFb(identifier, content); From 53fd04a8867a9e67519df38600d2a4c08b0e3065 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 11 Dec 2018 16:50:40 -0800 Subject: [PATCH 14/51] Revert "hooks for custom control sequences" This reverts commit 4d660deff3db9f3eea3923dcaa970fde0e9deaf8. --- src/EscapeSequenceParser.ts | 74 ++++--------------------------------- src/InputHandler.ts | 17 +-------- src/Terminal.ts | 6 +-- src/Types.ts | 14 ------- src/public/Terminal.ts | 5 +-- 5 files changed, 12 insertions(+), 104 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 90405a42..f4898841 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -303,39 +303,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } - private _removeHandler(array: any[], callback: any): void { - if (array) { - for (let i = array.length; --i >= 0; ) { - if (array[i] == callback) { - array.splice(i, 1); - return; - } - } - } - } - - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - let index = flag.charCodeAt(0); - let array = this._csiHandlers[index]; - if (! array) { this._csiHandlers[index] = array = new Array(); } - array.push(callback); - } - - removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - let index = flag.charCodeAt(0); - let array = this._csiHandlers[index]; - this._removeHandler(array, callback); - if (array && array.length == 0) - delete this._csiHandlers[index]; - } - /* deprecated */ setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { - this.clearCsiHandler(flag); - this.addCsiHandler(flag, (params: number[], collect: string): boolean => { - callback(params, collect); return true; - }); + this._csiHandlers[flag.charCodeAt(0)] = callback; } - /* deprecated */ clearCsiHandler(flag: string): void { if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; } @@ -353,25 +323,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlerFb = callback; } - addOscHandler(ident: number, callback: (data: string) => boolean): void { - let array = this._oscHandlers[ident]; - if (! array) { this._oscHandlers[ident] = array = new Array(); } - array.push(callback); - } - removeOscHandler(ident: number, callback: (data: string) => boolean): void { - let array = this._oscHandlers[ident]; - this._removeHandler(array, callback); - if (array && array.length == 0) - delete this._oscHandlers[ident]; - } - /* deprecated */ setOscHandler(ident: number, callback: (data: string) => void): void { - this.clearOscHandler(ident); - this.addOscHandler(ident, (data: string): boolean => { - callback(data); return true; - }); + this._oscHandlers[ident] = callback; } - /* deprecated */ clearOscHandler(ident: number): void { if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; } @@ -507,15 +461,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.CSI_DISPATCH: - let cHandler = this._csiHandlers[code]; - if (cHandler) { - for (let i = cHandler.length; ;) { - if (--i < 0) { cHandler = null; break; } - if ((cHandler[i])(params, collect)) - break; - } - } - if (! cHandler) this._csiHandlerFb(collect, params, code); + callback = this._csiHandlers[code]; + if (callback) callback(params, collect); + else this._csiHandlerFb(collect, params, code); break; case ParserAction.PARAM: if (code === 0x3b) params.push(0); @@ -590,15 +538,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // or with an explicit NaN OSC handler const identifier = parseInt(osc.substring(0, idx)); const content = osc.substring(idx + 1); - let oHandler = this._oscHandlers[identifier]; - if (oHandler) { - for (let i = oHandler.length; ;) { - if (--i < 0) { oHandler = null; break; } - if ((oHandler[i])(content)) - break; - } - } - if (! oHandler) this._oscHandlerFb(identifier, content); + callback = this._oscHandlers[identifier]; + if (callback) callback(content); + else this._oscHandlerFb(identifier, content); } } if (code === 0x1b) transition |= ParserState.ESCAPE; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 2c931a2b..7604b01f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IVtInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; @@ -112,7 +112,7 @@ class DECRQSS implements IDcsHandler { * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand * each function's header comment. */ -export class InputHandler extends Disposable implements IVtInputHandler { +export class InputHandler extends Disposable implements IInputHandler { private _surrogateFirst: string; constructor( @@ -465,19 +465,6 @@ export class InputHandler extends Disposable implements IVtInputHandler { this._terminal.updateRange(buffer.y); } - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - this._parser.addCsiHandler(flag, callback); - } - removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - this._parser.removeCsiHandler(flag, callback); - } - addOscHandler(ident: number, callback: (data: string) => boolean): void { - this._parser.setOscHandler(ident, callback); - } - removeOscHandler(ident: number, callback: (data: string) => boolean): void { - this._parser.removeOscHandler(ident, callback); - } - /** * BEL * Bell (Ctrl-G). diff --git a/src/Terminal.ts b/src/Terminal.ts index 8f66ebe9..bc8fb103 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IInputHandler, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -1287,10 +1287,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.refresh(0, this.rows - 1); } - public get inputHandler(): IInputHandler { - return this._inputHandler; - } - /** * Scroll the display of the terminal by a number of pages. * @param pageCount The number of pages to scroll (negative scrolls up). diff --git a/src/Types.ts b/src/Types.ts index 22eff1c8..a5aa8add 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -182,16 +182,6 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; } -/* - * An InputHandler for VT-style terminals - */ -export interface IVtInputHandler extends IInputHandler { - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; - removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; - addOscHandler(ident: number, callback: (data: string) => boolean): void; - removeOscHandler(ident: number, callback: (data: string) => boolean): void; -} - export interface ILinkMatcher { id: number; regex: RegExp; @@ -502,10 +492,6 @@ export interface IEscapeSequenceParser extends IDisposable { setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; clearCsiHandler(flag: string): void; setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; - removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; - addOscHandler(ident: number, callback: (data: string) => boolean): void; - removeOscHandler(ident: number, callback: (data: string) => boolean): void; setEscHandler(collectAndFlag: string, callback: () => void): void; clearEscHandler(collectAndFlag: string): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index de15fad0..8ff7cf2b 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -4,7 +4,7 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm'; -import { ITerminal, IInputHandler } from '../Types'; +import { ITerminal } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; @@ -15,9 +15,6 @@ export class Terminal implements ITerminalApi { this._core = new TerminalCore(options); } - public get inputHandler(): IInputHandler { - return (this._core as TerminalCore).inputHandler; - } public get element(): HTMLElement { return this._core.element; } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } From 8ceea112f7a4d11532b03d39a06d319c08c13f18 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 11 Dec 2018 17:18:10 -0800 Subject: [PATCH 15/51] hooks for custom control sequences This re-implements addCsiHandler/addOscHandler to return an IDisposable. --- src/EscapeSequenceParser.ts | 53 ++++++++++++++++++++++++++++++++++++- src/InputHandler.ts | 12 +++++++-- src/Terminal.ts | 6 ++++- src/Types.ts | 10 +++++++ src/public/Terminal.ts | 5 +++- 5 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index f4898841..28562939 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -4,6 +4,7 @@ */ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; +import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; /** @@ -41,7 +42,7 @@ export class TransitionTable { * @param action parser action to be done * @param next next parser state */ - add(code: number, state: number, action: number | null, next: number | null): void { + add(code: number, state: number, action: number | null, next: number | null): void { this.table[state << 8 | code] = ((action | 0) << 4) | ((next === undefined) ? state : next); } @@ -303,6 +304,32 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + const index = flag.charCodeAt(0); + const oldHead = this._csiHandlers[index]; + const newHead = Object.assign( + (params: number[], collect: string): void => { + if (callback(params, collect)) { } + else if (newHead.nextHandler) { newHead.nextHandler(params, collect); } + else { this._csiHandlerFb(collect, params, index); } + }, + { nextHandler: oldHead, + dispose(): void { + let previous = null; let cur = this._csiHandlers[index]; + for (; cur && cur.nextHandler; + previous = cur, cur = cur.nextHandler) { + if (cur === newHead) { + if (previous) { previous.nextHandler = cur.nextHandler; } + else { this._csiHandlers[index] = cur.nextHandler; } + break; + } + } + } + }); + this._csiHandlers[index] = newHead; + return newHead; + } + setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { this._csiHandlers[flag.charCodeAt(0)] = callback; } @@ -323,6 +350,30 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlerFb = callback; } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + const oldHead = this._oscHandlers[ident]; + const newHead = Object.assign( + (data: string): void => { + if (callback(data)) { } + else if (newHead.nextHandler) { newHead.nextHandler(data); } + else { this._oscHandlerFb(ident, data); } + }, + { nextHandler: oldHead, + dispose(): void { + let previous = null; let cur = this._oscHandlers[ident]; + for (; cur && cur.nextHandler; + previous = cur, cur = cur.nextHandler) { + if (cur === newHead) { + if (previous) { previous.nextHandler = cur.nextHandler; } + else { this._oscHandlers[ident] = cur.nextHandler; } + break; + } + } + } + }); + this._oscHandlers[ident] = newHead; + return newHead; + } setOscHandler(ident: number, callback: (data: string) => void): void { this._oscHandlers[ident] = callback; } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7604b01f..2e016296 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IVtInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; @@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; +import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; /** @@ -112,7 +113,7 @@ class DECRQSS implements IDcsHandler { * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand * each function's header comment. */ -export class InputHandler extends Disposable implements IInputHandler { +export class InputHandler extends Disposable implements IVtInputHandler { private _surrogateFirst: string; constructor( @@ -465,6 +466,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.y); } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._parser.addCsiHandler(flag, callback); + } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._parser.addOscHandler(ident, callback); + } + /** * BEL * Bell (Ctrl-G). diff --git a/src/Terminal.ts b/src/Terminal.ts index bc8fb103..8f66ebe9 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; +import { IInputHandlingTerminal, IInputHandler, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -1287,6 +1287,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.refresh(0, this.rows - 1); } + public get inputHandler(): IInputHandler { + return this._inputHandler; + } + /** * Scroll the display of the terminal by a number of pages. * @param pageCount The number of pages to scroll (negative scrolls up). diff --git a/src/Types.ts b/src/Types.ts index a5aa8add..e150b90b 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -182,6 +182,14 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; } +/* + * An InputHandler for VT-style terminals + */ +export interface IVtInputHandler extends IInputHandler { + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; +} + export interface ILinkMatcher { id: number; regex: RegExp; @@ -492,6 +500,8 @@ export interface IEscapeSequenceParser extends IDisposable { setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; clearCsiHandler(flag: string): void; setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; setEscHandler(collectAndFlag: string, callback: () => void): void; clearEscHandler(collectAndFlag: string): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 8ff7cf2b..de15fad0 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -4,7 +4,7 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm'; -import { ITerminal } from '../Types'; +import { ITerminal, IInputHandler } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; @@ -15,6 +15,9 @@ export class Terminal implements ITerminalApi { this._core = new TerminalCore(options); } + public get inputHandler(): IInputHandler { + return (this._core as TerminalCore).inputHandler; + } public get element(): HTMLElement { return this._core.element; } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } From 5af4626ec7d44f0536024a7a9af66b463ee5d7d9 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 13 Dec 2018 17:33:33 -0800 Subject: [PATCH 16/51] Change addCsiHandler/addOscHandler to not use Object.assign. --- src/EscapeSequenceParser.ts | 47 ++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 28562939..a104d583 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -307,25 +307,26 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { const index = flag.charCodeAt(0); const oldHead = this._csiHandlers[index]; - const newHead = Object.assign( + const parser = this; + const newHead = (params: number[], collect: string): void => { - if (callback(params, collect)) { } - else if (newHead.nextHandler) { newHead.nextHandler(params, collect); } - else { this._csiHandlerFb(collect, params, index); } - }, - { nextHandler: oldHead, - dispose(): void { - let previous = null; let cur = this._csiHandlers[index]; + if (! callback(params, collect)) { + if (newHead.nextHandler) { newHead.nextHandler(params, collect); } + else { this._csiHandlerFb(collect, params, index); } + } + }; + newHead.nextHandler = oldHead; + newHead.dispose = function (): void { + let previous = null; let cur = parser._csiHandlers[index]; for (; cur && cur.nextHandler; previous = cur, cur = cur.nextHandler) { if (cur === newHead) { if (previous) { previous.nextHandler = cur.nextHandler; } - else { this._csiHandlers[index] = cur.nextHandler; } + else { parser._csiHandlers[index] = cur.nextHandler; } break; } } - } - }); + }; this._csiHandlers[index] = newHead; return newHead; } @@ -352,25 +353,27 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { const oldHead = this._oscHandlers[ident]; - const newHead = Object.assign( + const parser = this; + const newHead = (data: string): void => { - if (callback(data)) { } - else if (newHead.nextHandler) { newHead.nextHandler(data); } - else { this._oscHandlerFb(ident, data); } - }, - { nextHandler: oldHead, - dispose(): void { - let previous = null; let cur = this._oscHandlers[ident]; + if (! callback(data)) { + if (newHead.nextHandler) { newHead.nextHandler(data); } + else { this._oscHandlerFb(ident, data); } + } + }; + newHead.nextHandler = oldHead; + newHead.dispose = + function (): void { + let previous = null; let cur = parser._oscHandlers[ident]; for (; cur && cur.nextHandler; previous = cur, cur = cur.nextHandler) { if (cur === newHead) { if (previous) { previous.nextHandler = cur.nextHandler; } - else { this._oscHandlers[ident] = cur.nextHandler; } + else { parser._oscHandlers[ident] = cur.nextHandler; } break; } } - } - }); + }; this._oscHandlers[ident] = newHead; return newHead; } From 8a5a03238fedded95e4cedfc9e80e3e0aec2ecc4 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 15 Dec 2018 09:42:19 -0800 Subject: [PATCH 17/51] New method _linkHandler used by both addCsiHandler and addOscHandler. --- src/EscapeSequenceParser.ts | 60 +++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index a104d583..c85f670d 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -7,6 +7,10 @@ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceP import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; +interface IHandlerLink extends IDisposable { + nextHandler: IHandlerLink | null; +} + /** * Returns an array filled with numbers between the low and high parameters (right exclusive). * @param low The low number. @@ -304,33 +308,38 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { - const index = flag.charCodeAt(0); - const oldHead = this._csiHandlers[index]; - const parser = this; - const newHead = - (params: number[], collect: string): void => { - if (! callback(params, collect)) { - if (newHead.nextHandler) { newHead.nextHandler(params, collect); } - else { this._csiHandlerFb(collect, params, index); } - } - }; - newHead.nextHandler = oldHead; + private _linkHandler(handlers: object[], index: number, newCallback: object): IDisposable { + const newHead: any = newCallback; + newHead.nextHandler = handlers[index] as IHandlerLink; newHead.dispose = function (): void { - let previous = null; let cur = parser._csiHandlers[index]; + let previous = null; + let cur = handlers[index] as IHandlerLink; for (; cur && cur.nextHandler; previous = cur, cur = cur.nextHandler) { if (cur === newHead) { if (previous) { previous.nextHandler = cur.nextHandler; } - else { parser._csiHandlers[index] = cur.nextHandler; } + else { handlers[index] = cur.nextHandler; } break; } } }; - this._csiHandlers[index] = newHead; + handlers[index] = newHead; return newHead; } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + const index = flag.charCodeAt(0); + const newHead = + (params: number[], collect: string): void => { + if (! callback(params, collect)) { + const next = (newHead as unknown as IHandlerLink).nextHandler; + if (next) { (next as any)(params, collect); } + else { this._csiHandlerFb(collect, params, index); } + } + }; + return this._linkHandler(this._csiHandlers, index, newHead); + } + setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { this._csiHandlers[flag.charCodeAt(0)] = callback; } @@ -352,30 +361,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - const oldHead = this._oscHandlers[ident]; - const parser = this; const newHead = (data: string): void => { if (! callback(data)) { - if (newHead.nextHandler) { newHead.nextHandler(data); } + const next = (newHead as unknown as IHandlerLink).nextHandler; + if (next) { (next as any)(data); } else { this._oscHandlerFb(ident, data); } } }; - newHead.nextHandler = oldHead; - newHead.dispose = - function (): void { - let previous = null; let cur = parser._oscHandlers[ident]; - for (; cur && cur.nextHandler; - previous = cur, cur = cur.nextHandler) { - if (cur === newHead) { - if (previous) { previous.nextHandler = cur.nextHandler; } - else { parser._oscHandlers[ident] = cur.nextHandler; } - break; - } - } - }; - this._oscHandlers[ident] = newHead; - return newHead; + return this._linkHandler(this._oscHandlers, ident, newHead); } setOscHandler(ident: number, callback: (data: string) => void): void { this._oscHandlers[ident] = callback; From 6b65ebd4aace41ddf0f7d663e5ff6c92a4a37948 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 15 Dec 2018 11:34:15 -0800 Subject: [PATCH 18/51] Various typing and API fixes, doc comments, typing test etc. --- fixtures/typings-test/typings-test.ts | 8 +++++++- src/InputHandler.ts | 4 ++-- src/Terminal.ts | 15 ++++++++++----- src/Types.ts | 8 -------- src/public/Terminal.ts | 11 +++++++---- src/ui/TestUtils.test.ts | 6 ++++++ typings/xterm.d.ts | 25 +++++++++++++++++++++++++ 7 files changed, 57 insertions(+), 20 deletions(-) diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 13da6961..87d911c2 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -4,7 +4,7 @@ /// -import { Terminal } from 'xterm'; +import { Terminal, IDisposable } from 'xterm'; namespace constructor { { @@ -119,6 +119,12 @@ namespace methods_core { const t: Terminal = new Terminal(); t.attachCustomKeyEventHandler((e: KeyboardEvent) => true); t.attachCustomKeyEventHandler((e: KeyboardEvent) => false); + const d1: IDisposable = t.addCsiHandler("x", + (params: number[], collect: string): boolean => params[0]===1); + d1.dispose(); + const d2: IDisposable = t.addOscHandler(199, + (data: string): boolean => true); + d2.dispose(); } namespace options { { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 2e016296..eb1ca105 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IVtInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; @@ -113,7 +113,7 @@ class DECRQSS implements IDcsHandler { * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand * each function's header comment. */ -export class InputHandler extends Disposable implements IVtInputHandler { +export class InputHandler extends Disposable implements IInputHandler { private _surrogateFirst: string; constructor( diff --git a/src/Terminal.ts b/src/Terminal.ts index 8f66ebe9..bed45e46 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IInputHandler, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -1287,10 +1287,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.refresh(0, this.rows - 1); } - public get inputHandler(): IInputHandler { - return this._inputHandler; - } - /** * Scroll the display of the terminal by a number of pages. * @param pageCount The number of pages to scroll (negative scrolls up). @@ -1417,6 +1413,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._customKeyEventHandler = customKeyEventHandler; } + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ + public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._inputHandler.addCsiHandler(flag, callback); + } + /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._inputHandler.addOscHandler(ident, callback); + } + /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. diff --git a/src/Types.ts b/src/Types.ts index e150b90b..93ba01f6 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -182,14 +182,6 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; } -/* - * An InputHandler for VT-style terminals - */ -export interface IVtInputHandler extends IInputHandler { - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; -} - export interface ILinkMatcher { id: number; regex: RegExp; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index de15fad0..87fcfaef 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -4,7 +4,7 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm'; -import { ITerminal, IInputHandler } from '../Types'; +import { ITerminal } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; @@ -15,9 +15,6 @@ export class Terminal implements ITerminalApi { this._core = new TerminalCore(options); } - public get inputHandler(): IInputHandler { - return (this._core as TerminalCore).inputHandler; - } public get element(): HTMLElement { return this._core.element; } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } @@ -62,6 +59,12 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } + public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._core.addCsiHandler(flag, callback); + } + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._core.addOscHandler(ident, callback); + } public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number { return this._core.registerLinkMatcher(regex, handler, options); } diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 10033a33..e6e4aaa3 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -54,6 +54,12 @@ export class MockTerminal implements ITerminal { attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { throw new Error('Method not implemented.'); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 7528bb55..cf489a61 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -481,6 +481,31 @@ declare module 'xterm' { */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; + /** + * (EXPERIMENTAL) Adds a handler for CSI escape sequences. + * @param flag The flag should be one-character string, which specifies + * the final character (e.g "m" for SGR) of the CSI sequence. + * @param callback The function to handle the escape sequence. + * The callback is called with the numerical params, + * as well as the special characters (e.g. "$" for DECSCPP). + * Return true if the sequence was handled; false if we should + * try a previous handler (set by addCsiHandler or setCsiHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + + /** + * (EXPERIMENTAL) Adds a handler for OSC escape sequences. + * @param ident The number (first parameter) of the sequence. + * @param callback The function to handle the escape sequence. + * The callback is called with OSC data string. + * Return true if the sequence was handled; false if we should + * try a previous handler (set by addOscHandler or setOscHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; /** * (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to * be matched and handled. From 5460068d52154726fda3179fbbe53fa615662ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 17 Dec 2018 23:59:48 +0100 Subject: [PATCH 19/51] fix invalid string and buffer index --- src/Buffer.test.ts | 33 +++++++++++++++++++++++++++++++-- src/Buffer.ts | 2 +- src/Linkifier.ts | 8 ++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 6b6adc11..5c1e56c9 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -355,7 +355,7 @@ describe('Buffer', () => { let terminal: TestTerminal; beforeEach(() => { - terminal = new TestTerminal({rows: 5, cols: 10}); + terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); }); it('multiline ascii', () => { @@ -516,9 +516,38 @@ describe('Buffer', () => { assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); } }); + + it('test fully wrapped buffer up to last char', () => { + const input = Array(6).join('1234567890'); + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); + } + }); + + it('test fully wrapped buffer up to last char with full width odd', () => { + const input = 'aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301' + + 'aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.equal( + (!(i % 3)) + ? input[i] + : (i % 3 === 1) + ? input.substr(i, 2) + : input.substr(i-1, 2), + terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); + } + }); }); describe('BufferStringIterator', function(): void { - it('iterator does not ovrflow buffer limits', function(): void { + it('iterator does not overflow buffer limits', function(): void { const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); const data = [ 'aaaaaaaaaa', diff --git a/src/Buffer.ts b/src/Buffer.ts index 10f1d1b2..df4134ee 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -252,7 +252,7 @@ export class Buffer implements IBuffer { while (stringIndex) { const line = this.lines.get(lineIndex); if (!line) { - [-1, -1]; + return [-1, -1]; } for (let i = 0; i < line.length; ++i) { stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length; diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 2ec3ed40..53247c95 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -219,9 +219,17 @@ export class Linkifier extends EventEmitter implements ILinkifier { // also correct regex and string search offsets for the next loop run stringIndex = text.indexOf(uri, stringIndex + 1); rex.lastIndex = stringIndex + uri.length; + if (stringIndex < 0) { + // invalid stringIndex (should not have happened) + break; + } // get the buffer index as [absolute row, col] for the match const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex); + if (bufferIndex[0] < 0) { + // invalid bufferIndex (should not have happened) + break; + } const line = this._terminal.buffer.lines.get(bufferIndex[0]); const char = line.get(bufferIndex[1]); From 0da630010279cbebd4363adb44991ba7c2652373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 18 Dec 2018 10:50:30 +0100 Subject: [PATCH 20/51] make linter happy --- src/Buffer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 5c1e56c9..0546dfe8 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -541,7 +541,7 @@ describe('Buffer', () => { ? input[i] : (i % 3 === 1) ? input.substr(i, 2) - : input.substr(i-1, 2), + : input.substr(i - 1, 2), terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); } }); From 04ac36fc1611a35ae22dea7ae1a9db4d39a0adeb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 18 Dec 2018 16:22:41 -0800 Subject: [PATCH 21/51] Fix translateToString endCol default arg --- src/Buffer.ts | 2 +- src/BufferLine.test.ts | 5 +++++ src/BufferLine.ts | 14 ++++++-------- src/SelectionManager.ts | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index df4134ee..74750a8b 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -275,7 +275,7 @@ export class Buffer implements IBuffer { * @param startCol The column to start at. * @param endCol The column to end at. */ - public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string { + public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string { const line = this.lines.get(lineIndex); if (!line) { return ''; diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 93b2759f..fbf8b051 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -325,5 +325,10 @@ describe('BufferLine', function(): void { chai.expect(line.translateToString(false)).equal(' '); chai.expect(line.translateToString(true)).equal(''); }); + it('should work with endCol=0', () => { + const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false); + line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); + chai.expect(line.translateToString(true, 0, 0)).equal(''); + }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index bcc3d1bb..3f93af62 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -118,13 +118,12 @@ export class BufferLineJSArray implements IBufferLine { return 0; } - public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = null): string { - let length = endCol || this.length; + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { if (trimRight) { - length = Math.min(length, this.getTrimmedLength()); + endCol = Math.min(endCol, this.getTrimmedLength()); } let result = ''; - while (startCol < length) { + while (startCol < endCol) { result += this.get(startCol)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; startCol += this.get(startCol)[CHAR_DATA_WIDTH_INDEX] || 1; } @@ -305,13 +304,12 @@ export class BufferLine implements IBufferLine { return 0; } - public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = null): string { - let length = endCol || this.length; + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { if (trimRight) { - length = Math.min(length, this.getTrimmedLength()); + endCol = Math.min(endCol, this.getTrimmedLength()); } let result = ''; - while (startCol < length) { + while (startCol < endCol) { const stringData = this._data[startCol * CELL_SIZE + Cell.STRING]; result += (stringData & IS_COMBINED_BIT_MASK) ? this._combined[startCol] : (stringData) ? String.fromCharCode(stringData) : WHITESPACE_CELL_CHAR; startCol += this._data[startCol * CELL_SIZE + Cell.WIDTH] || 1; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 4bac0400..1aea1cb5 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -198,7 +198,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } } else { // Get first row - const startRowEndCol = start[1] === end[1] ? end[0] : null; + const startRowEndCol = start[1] === end[1] ? end[0] : undefined; result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); // Get middle rows From 9cd7bf2af1178c29d6be4254bd13ed0c191ae299 Mon Sep 17 00:00:00 2001 From: Linmiao Xu Date: Wed, 19 Dec 2018 13:11:45 +0900 Subject: [PATCH 22/51] Weblinks should not allow quotes at end of urls enclosed in quotes --- src/addons/webLinks/webLinks.test.ts | 24 ++++++++++++++++++++++++ src/addons/webLinks/webLinks.ts | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts index 8ada2510..1e8a4ae7 100644 --- a/src/addons/webLinks/webLinks.test.ts +++ b/src/addons/webLinks/webLinks.test.ts @@ -63,4 +63,28 @@ describe('webLinks addon', () => { assert.equal(uri, 'http://foo.com/colon:test'); }); + + it('should not allow " character at the end of a URI enclosed with ""', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '"http://foo.com/"'; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/'); + }); + + it('should not allow \' character at the end of a URI enclosed with \'\'', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '\'http://foo.com/\''; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/'); + }); }); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index 75d79104..f0d69cc5 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -14,7 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; const localHostClause = '(localhost)'; const portClause = '(:\\d{1,5})'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; -const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:\\s])'; +const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])'; const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; From 86f194a6d34590ac3098480137c0e7ff7abf098b Mon Sep 17 00:00:00 2001 From: Linmiao Xu Date: Wed, 19 Dec 2018 13:50:41 +0900 Subject: [PATCH 23/51] Fix npm scripts when using node 10+ --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7ebdbf3c..01de5ab7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4479,9 +4479,9 @@ nanomatch@^1.2.9: to-regex "^3.0.1" natives@^1.1.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.4.tgz#2f0f224fc9a7dd53407c7667c84cf8dbe773de58" - integrity sha512-Q29yeg9aFKwhLVdkTAejM/HvYG0Y1Am1+HUkFQGn5k2j8GS+v60TVmZh6nujpEAj/qql+wGUrlryO8bF+b1jEg== + version "1.1.6" + resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.6.tgz#a603b4a498ab77173612b9ea1acdec4d980f00bb" + integrity sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA== needle@^2.2.1: version "2.2.1" From 5ebd543b8b5388f396f6637732de1ea4a10f4f3f Mon Sep 17 00:00:00 2001 From: Linmiao Xu Date: Wed, 19 Dec 2018 14:32:15 +0900 Subject: [PATCH 24/51] Fix missing quotes in import statement in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c654f288..86ac1f09 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Addons are JavaScript modules that extend the `Terminal` prototype with new meth To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method: ```javascript -import { Terminal } from xterm; +import { Terminal } from 'xterm'; import * as fit from 'xterm/lib/addons/fit/fit'; From 1104bcbccb523a856f2181060fc34ff73e4a3d48 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 20 Dec 2018 09:41:44 -0500 Subject: [PATCH 25/51] Allow holding key on mac to send multiple keys to terminal --- src/core/input/Keyboard.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 8c6f3c59..eccc3769 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,6 +349,8 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key !== 'Shift') { + result.key = ev.key; } break; } From ec1060187d4789508a779744ab208efb0a92ca61 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 20 Dec 2018 09:50:18 -0500 Subject: [PATCH 26/51] Add tests --- src/core/input/Keyboard.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/core/input/Keyboard.test.ts b/src/core/input/Keyboard.test.ts index e9846831..e0735a4a 100644 --- a/src/core/input/Keyboard.test.ts +++ b/src/core/input/Keyboard.test.ts @@ -281,5 +281,15 @@ describe('Keyboard', () => { assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B'); assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB'); }); + + it('should handle lowercase letters', () => { + assert.equal(testEvaluateKeyboardEvent({ keyCode: 65, key: 'a' }).key, 'a'); + assert.equal(testEvaluateKeyboardEvent({ keyCode: 189, key: '-' }).key, '-'); + }); + + it('should handle uppercase letters', () => { + assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 65, key: 'A' }).key, 'A'); + }); + }); }); From b820f23da6817e299ca18e11bbd1008c45b0a024 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 20 Dec 2018 09:50:57 -0500 Subject: [PATCH 27/51] Add additional test --- src/core/input/Keyboard.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/input/Keyboard.test.ts b/src/core/input/Keyboard.test.ts index e0735a4a..a0dc3cbc 100644 --- a/src/core/input/Keyboard.test.ts +++ b/src/core/input/Keyboard.test.ts @@ -289,6 +289,7 @@ describe('Keyboard', () => { it('should handle uppercase letters', () => { assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 65, key: 'A' }).key, 'A'); + assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!'); }); }); From 0782fbb8284a0813d1edd8e11931f3977397a86e Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 20 Dec 2018 09:57:24 -0500 Subject: [PATCH 28/51] Fix check --- src/core/input/Keyboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index eccc3769..31e97e35 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,7 +349,7 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } - } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key !== 'Shift') { + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 65) { result.key = ev.key; } break; From 64dd9d46fbe18720651ad218ef39375d414fece7 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 20 Dec 2018 14:21:35 -0500 Subject: [PATCH 29/51] Change keyCode cutoff --- src/core/input/Keyboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 31e97e35..21c81a81 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,7 +349,7 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } - } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 65) { + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48) { result.key = ev.key; } break; From ff73f705aeaaff6d1cbeb3ccdbb557c45e169890 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 20 Dec 2018 11:47:51 -0800 Subject: [PATCH 30/51] Fix winptyCompat wrapped line heuristic Part of Microsoft/vscode#65485 --- src/addons/winptyCompat/winptyCompat.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 2513dce6..aec580ed 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -7,7 +7,8 @@ import { Terminal } from 'xterm'; import { IWinptyCompatAddonTerminal } from './Interfaces'; const CHAR_DATA_CODE_INDEX = 3; -const NULL_CELL_CODE = 32; +const NULL_CELL_CODE = 0; +const WHITESPACE_CELL_CODE = 32; export function winptyCompatInit(terminal: Terminal): void { const addonTerminal = terminal; @@ -32,7 +33,7 @@ export function winptyCompatInit(terminal: Terminal): void { const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1); const lastChar = line.get(addonTerminal.cols - 1); - if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) { + if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) { const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y); nextLine.isWrapped = true; } From 3eac446a854c253a7bec6475c3736b48f3ea4c18 Mon Sep 17 00:00:00 2001 From: Robin Sturm Date: Thu, 20 Dec 2018 11:54:26 -0800 Subject: [PATCH 31/51] bind function before calling --- src/addons/fullscreen/fullscreen.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/addons/fullscreen/fullscreen.ts b/src/addons/fullscreen/fullscreen.ts index 4d05e904..083c00c2 100644 --- a/src/addons/fullscreen/fullscreen.ts +++ b/src/addons/fullscreen/fullscreen.ts @@ -22,6 +22,7 @@ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void { fn = term.element.classList.add; } + fn = fn.bind(term.element.classList); fn('fullscreen'); } From a4b4d082fb1f0a84105554fc4c42edb07bbaf362 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Fri, 21 Dec 2018 07:38:14 -0500 Subject: [PATCH 32/51] Don't include num lock and scroll lock --- src/core/input/Keyboard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 21c81a81..37e663f5 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,7 +349,8 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } - } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48) { + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 + && ev.keyCode !== 144 && ev.keyCode !== 145) { // Include only keys that that result in a character; don't include num lock and scroll lock result.key = ev.key; } break; From 78e7e39bb48c17e197d1b6080635289dbdf3cbf4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 21 Dec 2018 23:04:38 -0800 Subject: [PATCH 33/51] Improve the readme --- README.md | 57 +++++++++++++++++++------------------------------------ 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 86ac1f09..35ccc9b6 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,20 @@ [![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) -[![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) -Xterm.js is a terminal front-end component written in JavaScript that works in the browser. - -It enables applications to provide fully featured terminals to their users and create great development experiences. +Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia. ## Features -- **Text-based application support**: Use xterm.js to work with applications like `bash`, `git` etc. -- **Curses-based application support**: Use xterm.js to work with applications like `vim`, `tmux` etc. -- **Mouse events support**: Xterm.js captures mouse events like click and scroll and passes them to the terminal's back-end controlling process -- **CJK (Chinese, Japanese, Korean) character support**: Xterm.js renders CJK characters seamlessly -- **IME support**: Insert international (including CJK) characters using IME input with your keyboard -- **Self-contained library**: Xterm.js works on its own. It does not require any external libraries like jQuery or React to work -- **Modular, event-based API**: Lets you build addons and themes with ease + +- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support +- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer +- **Rich unicode support**: Supports CJK, emojis and IMEs +- **Self-contained**: Requires zero dependencies to work +- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option +- **And much more**: Links, theming, addons, well documented API, etc. ## What xterm.js is not + - Xterm.js is not a terminal application that you can download and use on your computer - Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output) @@ -30,7 +27,7 @@ First you need to install the module, we ship exclusively through [npm](https:// npm install xterm ``` -To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `
` onto which xterm can attach itself. +To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `
` onto which xterm can attach itself. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. ```html @@ -50,25 +47,17 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t ``` -Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. - ### Importing -The proposed way to load xterm.js is via the ES6 module syntax. +The recommended way to load xterm.js is via the ES6 module syntax: ```javascript import { Terminal } from 'xterm'; ``` -### API - -The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API. - -Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs. - ### Addons -Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API. +Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own by using the [public API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method: @@ -76,7 +65,6 @@ To use an addon, just import the JavaScript module and pass it to `Terminal`'s ` import { Terminal } from 'xterm'; import * as fit from 'xterm/lib/addons/fit/fit'; - Terminal.applyAddon(fit); var xterm = new Terminal(); // Instantiate the terminal @@ -87,25 +75,16 @@ You will also need to include the addon's CSS file if it has one in the folder. #### Importing Addons in TypeScript -There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(xterm).fit()`. - -Alternatively, you can import addon function and enhance the terminal on demand. This would have better typing support and is friendly to treeshaking. E.g.: +There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(xterm as any).fit()`. Alternatively, you can import the addon function and enhance the terminal on demand. This has better typing support and is friendly to treeshaking. ```typescript import { Terminal } from 'xterm'; import { fit } from 'xterm/lib/addons/fit/fit'; const xterm = new Terminal(); -// Fit the terminal when necessary: -fit(xterm); +fit(xterm); // Fit the terminal when necessary ``` -#### Third party addons - -There are also the following third party addons available: - -- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont) - ## Browser Support Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: @@ -116,11 +95,13 @@ Since xterm.js is typically implemented as a developer tool, only modern browser - Safari latest - IE11 -Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working. +Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working. ## API -The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using. +The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API. + +Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs. ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. @@ -182,7 +163,7 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht Xterm.js follows a monthly release cycle roughly. -The existing releases are available at this GitHub repo's [Releases](https://github.com/sourcelair/xterm.js/releases), while the roadmap is available as [Milestones](https://github.com/sourcelair/xterm.js/milestones). +All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), while a rough roadmap is available by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). ## Contributing From 81ada399892ccba0b2d03e6e75fb14594775a0e0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 21 Dec 2018 23:07:19 -0800 Subject: [PATCH 34/51] Remove Gitter link from issue template Fixes #1838 --- .github/ISSUE_TEMPLATE/question.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 10eee3d9..1fe5c763 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,8 +1,8 @@ --- name: Question -about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js. +about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs --- 🛑 The issue tracker is not for questions 🛑 -If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js. +If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs. From c6e6c519291be333792f0469b205f4522abd1814 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 23 Dec 2018 10:51:41 -0800 Subject: [PATCH 35/51] Use textBaseline middle to draw instead of top Chrome and Firefox behavior differs, see: - https://bugzilla.mozilla.org/show_bug.cgi?id=737852 - https://bugs.chromium.org/p/chromium/issues/detail?id=607053 Fixes #1858 --- src/renderer/BaseRenderLayer.ts | 8 ++++---- src/renderer/atlas/CharAtlasGenerator.ts | 10 +++++----- src/renderer/atlas/DynamicCharAtlas.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 2afdebb5..3e0b8643 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -236,12 +236,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { this._ctx.font = this._getFont(terminal, false, false); - this._ctx.textBaseline = 'top'; + this._ctx.textBaseline = 'middle'; this._clipRow(terminal, y); this._ctx.fillText( charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop); + (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); } /** @@ -295,7 +295,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void { this._ctx.save(); this._ctx.font = this._getFont(terminal, bold, italic); - this._ctx.textBaseline = 'top'; + this._ctx.textBaseline = 'middle'; if (fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.background.css; @@ -316,7 +316,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( chars, x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop); + (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); this._ctx.restore(); } diff --git a/src/renderer/atlas/CharAtlasGenerator.ts b/src/renderer/atlas/CharAtlasGenerator.ts index e40215cf..cadcce2e 100644 --- a/src/renderer/atlas/CharAtlasGenerator.ts +++ b/src/renderer/atlas/CharAtlasGenerator.ts @@ -29,7 +29,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.save(); ctx.fillStyle = config.colors.foreground.css; ctx.font = getFont(config.fontWeight, config); - ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle'; // Default color for (let i = 0; i < 256; i++) { @@ -37,7 +37,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.beginPath(); ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); ctx.clip(); - ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight / 2); ctx.restore(); } // Default color bold @@ -48,7 +48,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.beginPath(); ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); ctx.clip(); - ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight * 1.5); ctx.restore(); } ctx.restore(); @@ -64,7 +64,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.rect(i * cellWidth, y, cellWidth, cellHeight); ctx.clip(); ctx.fillStyle = config.colors.ansi[colorIndex].css; - ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2); ctx.restore(); } } @@ -80,7 +80,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.rect(i * cellWidth, y, cellWidth, cellHeight); ctx.clip(); ctx.fillStyle = config.colors.ansi[colorIndex].css; - ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2); ctx.restore(); } } diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index 72010768..e311c369 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -250,7 +250,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { const fontStyle = glyph.italic ? 'italic' : ''; this._tmpCtx.font = `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; - this._tmpCtx.textBaseline = 'top'; + this._tmpCtx.textBaseline = 'middle'; this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css; @@ -259,7 +259,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { this._tmpCtx.globalAlpha = DIM_OPACITY; } // Draw the character - this._tmpCtx.fillText(glyph.chars, 0, 0); + this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2); this._tmpCtx.restore(); // clear the background from the character to avoid issues with drawing over the previous From 48ff841d6ab23744570326201c4294a25ec16474 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 23 Dec 2018 12:27:26 -0800 Subject: [PATCH 36/51] Be more paranoid about cleaning up escape sequence handlers. --- src/EscapeSequenceParser.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index c85f670d..32155130 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -283,8 +283,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandlerFb = null; this._printHandler = null; this._executeHandlers = null; - this._csiHandlers = null; this._escHandlers = null; + let handlers; + while ((handlers = this._csiHandlers) && handlers.dispose !== undefined) { + handlers.dispose(); + if (handlers === this._csiHandlers) { break; } // sanity check + } + this._csiHandlers = null; + while ((handlers = this._oscHandlers) && handlers.dispose !== undefined) { + handlers.dispose(); + if (handlers === this._oscHandlers) { break; } // sanity check + } this._oscHandlers = null; this._dcsHandlers = null; this._activeDcsHandler = null; @@ -319,6 +328,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (cur === newHead) { if (previous) { previous.nextHandler = cur.nextHandler; } else { handlers[index] = cur.nextHandler; } + cur.nextHandler = null; + handlers = null; newCallback = null; // just in case break; } } From 9e3e724f63f0de83a86997041395bea58ad8009e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 10:53:40 -0800 Subject: [PATCH 37/51] Add sanity checks to dom renderer underline code Fixes #1860 --- src/renderer/dom/DomRenderer.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index a0cefd67..c5ef212d 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -364,9 +364,11 @@ export class DomRenderer extends EventEmitter implements IRenderer { return; } const span = row.children[x]; - span.style.textDecoration = enabled ? 'underline' : 'none'; - x = (x + 1) % cols; - if (x === 0) { + if (span) { + span.style.textDecoration = enabled ? 'underline' : 'none'; + } + if (++x >= cols) { + x = 0; y++; } } From d01efdda270f0d9c2a0c55ec78bc40e7213e1d11 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 14:08:53 -0800 Subject: [PATCH 38/51] Use array instead of linkedlist, add typings --- src/EscapeSequenceParser.ts | 110 ++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 61 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 32155130..52007244 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -11,6 +11,13 @@ interface IHandlerLink extends IDisposable { nextHandler: IHandlerLink | null; } +interface IHandlerCollection { + [key: string]: T[]; +} + +type CsiHandler = (params: number[], collect: string) => boolean | void; +type OscHandler = (data: string) => boolean | void; + /** * Returns an array filled with numbers between the low and high parameters (right exclusive). * @param low The low number. @@ -46,7 +53,7 @@ export class TransitionTable { * @param action parser action to be done * @param next next parser state */ - add(code: number, state: number, action: number | null, next: number | null): void { + add(code: number, state: number, action: number | null, next: number | null): void { this.table[state << 8 | code] = ((action | 0) << 4) | ((next === undefined) ? state : next); } @@ -227,9 +234,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // handler lookup containers protected _printHandler: (data: string, start: number, end: number) => void; protected _executeHandlers: any; - protected _csiHandlers: any; + protected _csiHandlers: IHandlerCollection; protected _escHandlers: any; - protected _oscHandlers: any; + protected _oscHandlers: IHandlerCollection; protected _dcsHandlers: any; protected _activeDcsHandler: IDcsHandler | null; protected _errorHandler: (state: IParsingState) => IParsingState; @@ -284,16 +291,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._printHandler = null; this._executeHandlers = null; this._escHandlers = null; - let handlers; - while ((handlers = this._csiHandlers) && handlers.dispose !== undefined) { - handlers.dispose(); - if (handlers === this._csiHandlers) { break; } // sanity check - } this._csiHandlers = null; - while ((handlers = this._oscHandlers) && handlers.dispose !== undefined) { - handlers.dispose(); - if (handlers === this._oscHandlers) { break; } // sanity check - } this._oscHandlers = null; this._dcsHandlers = null; this._activeDcsHandler = null; @@ -317,42 +315,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } - private _linkHandler(handlers: object[], index: number, newCallback: object): IDisposable { - const newHead: any = newCallback; - newHead.nextHandler = handlers[index] as IHandlerLink; - newHead.dispose = function (): void { - let previous = null; - let cur = handlers[index] as IHandlerLink; - for (; cur && cur.nextHandler; - previous = cur, cur = cur.nextHandler) { - if (cur === newHead) { - if (previous) { previous.nextHandler = cur.nextHandler; } - else { handlers[index] = cur.nextHandler; } - cur.nextHandler = null; - handlers = null; newCallback = null; // just in case - break; - } - } - }; - handlers[index] = newHead; - return newHead; - } - - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + addCsiHandler(flag: string, callback: CsiHandler): IDisposable { const index = flag.charCodeAt(0); - const newHead = - (params: number[], collect: string): void => { - if (! callback(params, collect)) { - const next = (newHead as unknown as IHandlerLink).nextHandler; - if (next) { (next as any)(params, collect); } - else { this._csiHandlerFb(collect, params, index); } - } - }; - return this._linkHandler(this._csiHandlers, index, newHead); + if (this._csiHandlers[index] === undefined) { + this._csiHandlers[index] = []; + } + this._csiHandlers[index].push(callback); + return { + dispose: () => this._csiHandlers[index].splice(this._csiHandlers[index].indexOf(callback)) + }; } - setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { - this._csiHandlers[flag.charCodeAt(0)] = callback; + this._csiHandlers[flag.charCodeAt(0)] = [callback]; } clearCsiHandler(flag: string): void { if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; @@ -372,18 +346,16 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - const newHead = - (data: string): void => { - if (! callback(data)) { - const next = (newHead as unknown as IHandlerLink).nextHandler; - if (next) { (next as any)(data); } - else { this._oscHandlerFb(ident, data); } - } - }; - return this._linkHandler(this._oscHandlers, ident, newHead); + if (this._oscHandlers[ident] === undefined) { + this._oscHandlers[ident] = []; + } + this._oscHandlers[ident].push(callback); + return { + dispose: () => this._oscHandlers[ident].splice(this._oscHandlers[ident].indexOf(callback)) + }; } setOscHandler(ident: number, callback: (data: string) => void): void { - this._oscHandlers[ident] = callback; + this._oscHandlers[ident] = [callback]; } clearOscHandler(ident: number): void { if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; @@ -520,9 +492,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.CSI_DISPATCH: - callback = this._csiHandlers[code]; - if (callback) callback(params, collect); - else this._csiHandlerFb(collect, params, code); + // Trigger CSI Handler + const handlers = this._csiHandlers[code]; + let j: number; + for (j = handlers.length - 1; j >= 0; j--) { + if (handlers[j](params, collect)) { + break; + } + } + if (j < 0) { + this._csiHandlerFb(collect, params, code); + } break; case ParserAction.PARAM: if (code === 0x3b) params.push(0); @@ -597,9 +577,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // or with an explicit NaN OSC handler const identifier = parseInt(osc.substring(0, idx)); const content = osc.substring(idx + 1); - callback = this._oscHandlers[identifier]; - if (callback) callback(content); - else this._oscHandlerFb(identifier, content); + // Trigger OSC Handler + const handlers = this._oscHandlers[identifier]; + let j: number; + for (j = handlers.length - 1; j >= 0; j--) { + if (handlers[j](content)) { + break; + } + } + if (j < 0) { + this._oscHandlerFb(identifier, content); + } } } if (code === 0x1b) transition |= ParserState.ESCAPE; From 38796a0f748340044f875580870764f80b5535f6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 14:40:23 -0800 Subject: [PATCH 39/51] Add tests, fix NPE --- src/EscapeSequenceParser.test.ts | 96 ++++++++++++++++++++++++++++++++ src/EscapeSequenceParser.ts | 12 ++-- 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index e92f4125..3b01cb8a 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1169,6 +1169,54 @@ describe('EscapeSequenceParser', function (): void { parser2.parse(INPUT); chai.expect(csi).eql([]); }); + describe('CSI custom handlers', () => { + it('Prevent fallback', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Allow fallback', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; }); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Multiple custom handlers fallback once', () => { + const csiCustom: [string, number[], string][] = []; + const csiCustom2: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Multiple custom handlers no fallback', () => { + const csiCustom: [string, number[], string][] = []; + const csiCustom2: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([], 'Should not fallback once'); + chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setCsiHandler('m', () => order.push(1)); + parser2.addCsiHandler('m', () => { order.push(2); return false; }); + parser2.addCsiHandler('m', () => { order.push(3); return false; }); + parser2.parse('\x1b[0m'); + chai.expect(order).eql([3, 2, 1]); + }); + }); it('EXECUTE handler', function (): void { parser2.setExecuteHandler('\n', function (): void { exe.push('\n'); @@ -1196,6 +1244,54 @@ describe('EscapeSequenceParser', function (): void { parser2.parse(INPUT); chai.expect(osc).eql([]); }); + describe('OSC custom handlers', () => { + it('Prevent fallback', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + }); + it('Allow fallback', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; }); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + }); + it('Multiple custom handlers fallback once', () => { + const oscCustom: [number, string][] = []; + const oscCustom2: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; }); + parser2.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + }); + it('Multiple custom handlers no fallback', () => { + const oscCustom: [number, string][] = []; + const oscCustom2: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; }); + parser2.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([], 'Should not fallback once'); + chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setOscHandler(1, () => order.push(1)); + parser2.addOscHandler(1, () => { order.push(2); return false; }); + parser2.addOscHandler(1, () => { order.push(3); return false; }); + parser2.parse('\x1b]1;foo=bar\x1b\\'); + chai.expect(order).eql([3, 2, 1]); + }); + }); it('DCS handler', function (): void { parser2.setDcsHandler('+p', { hook: function (collect: string, params: number[], flag: number): void { diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 52007244..acb5a42d 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -7,10 +7,6 @@ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceP import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; -interface IHandlerLink extends IDisposable { - nextHandler: IHandlerLink | null; -} - interface IHandlerCollection { [key: string]: T[]; } @@ -494,8 +490,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserAction.CSI_DISPATCH: // Trigger CSI Handler const handlers = this._csiHandlers[code]; - let j: number; - for (j = handlers.length - 1; j >= 0; j--) { + let j = handlers ? handlers.length - 1 : -1; + for (; j >= 0; j--) { if (handlers[j](params, collect)) { break; } @@ -579,8 +575,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP const content = osc.substring(idx + 1); // Trigger OSC Handler const handlers = this._oscHandlers[identifier]; - let j: number; - for (j = handlers.length - 1; j >= 0; j--) { + let j = handlers ? handlers.length - 1 : -1; + for (; j >= 0; j--) { if (handlers[j](content)) { break; } From c045c803d60dc569baab8efd53b6722faebd3e81 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 14:48:35 -0800 Subject: [PATCH 40/51] Add tests for dispose --- src/EscapeSequenceParser.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 3b01cb8a..9216dd4c 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1216,6 +1216,15 @@ describe('EscapeSequenceParser', function (): void { parser2.parse('\x1b[0m'); chai.expect(order).eql([3, 2, 1]); }); + it('Dispose', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + }); }); it('EXECUTE handler', function (): void { parser2.setExecuteHandler('\n', function (): void { @@ -1291,6 +1300,15 @@ describe('EscapeSequenceParser', function (): void { parser2.parse('\x1b]1;foo=bar\x1b\\'); chai.expect(order).eql([3, 2, 1]); }); + it('Dispose', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + }); }); it('DCS handler', function (): void { parser2.setDcsHandler('+p', { From adbb929f7881d6f678cdde8b8736bfd113565204 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 14:49:49 -0800 Subject: [PATCH 41/51] Wrap .d.ts comments to 80 chars --- typings/xterm.d.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index cf489a61..0ceab01d 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -483,14 +483,13 @@ declare module 'xterm' { /** * (EXPERIMENTAL) Adds a handler for CSI escape sequences. - * @param flag The flag should be one-character string, which specifies - * the final character (e.g "m" for SGR) of the CSI sequence. - * @param callback The function to handle the escape sequence. - * The callback is called with the numerical params, - * as well as the special characters (e.g. "$" for DECSCPP). - * Return true if the sequence was handled; false if we should - * try a previous handler (set by addCsiHandler or setCsiHandler). - * The most recently-added handler is tried first. + * @param flag The flag should be one-character string, which specifies the + * final character (e.g "m" for SGR) of the CSI sequence. + * @param callback The function to handle the escape sequence. The callback + * is called with the numerical params, as well as the special characters + * (e.g. "$" for DECSCPP). Return true if the sequence was handled; false if + * we should try a previous handler (set by addCsiHandler or setCsiHandler). + * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; @@ -498,11 +497,10 @@ declare module 'xterm' { /** * (EXPERIMENTAL) Adds a handler for OSC escape sequences. * @param ident The number (first parameter) of the sequence. - * @param callback The function to handle the escape sequence. - * The callback is called with OSC data string. - * Return true if the sequence was handled; false if we should - * try a previous handler (set by addOscHandler or setOscHandler). - * The most recently-added handler is tried first. + * @param callback The function to handle the escape sequence. The callback + * is called with OSC data string. Return true if the sequence was handled; + * false if we should try a previous handler (set by addOscHandler or + * setOscHandler). The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; From 204e42a1513fdbdeb0a664e2471161d770f8c2c1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 15:15:12 -0800 Subject: [PATCH 42/51] Fix incremental search --- src/addons/search/SearchHelper.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 408e1f29..d3040f73 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -49,8 +49,7 @@ export class SearchHelper implements ISearchHelper { // For incremental search, use existing row if (this._terminal.getSelection().length !== 0) { startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1]; - // TODO: Fix for incremental - startCol = this._terminal._core.selectionManager.selectionEnd[0]; + startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0]; } } From 76302e2146bb2724137a86aa73d6b3b3c4f709c6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 15:32:34 -0800 Subject: [PATCH 43/51] Get multiple matches working after incremental changes --- src/addons/search/SearchHelper.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index d3040f73..0a66fdff 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -52,11 +52,12 @@ export class SearchHelper implements ISearchHelper { startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0]; } } + console.log(`Start from ${startCol},${startRow}`); this._initLinesCache(); // Search from startRow to end - for (let y = incremental ? startRow : startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + for (let y = startRow; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { result = this._findInLine(term, { row: y, col: startCol }, searchOptions); if (result) { break; @@ -111,7 +112,7 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); // Search from startRow to top - for (let y = incremental ? startRow : startRow - 1; y >= 0; y--) { + for (let y = startRow; y >= 0; y--) { result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); if (result) { break; @@ -216,6 +217,7 @@ export class SearchHelper implements ISearchHelper { resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length); } else { resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col); + console.log('resultIndex', resultIndex); } } From e0d535e0da1050aaa19860747c0cdedf6017cd38 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 15:49:08 -0800 Subject: [PATCH 44/51] Fix previous search, remove incremental previous search --- demo/client.ts | 6 ++-- src/addons/search/Interfaces.ts | 5 ++- src/addons/search/SearchHelper.ts | 57 ++++++++++++++++++------------- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index d99ff66d..acc83cb8 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -113,9 +113,9 @@ function createTerminal(): void { }); addDomListener(actionElements.findPrevious, 'keyup', (e) => { - const searchOptions = getSearchOptions(); - searchOptions.incremental = e.key !== `Enter`; - term.findPrevious(actionElements.findPrevious.value, searchOptions); + if (e.key === `Enter`) { + term.findPrevious(actionElements.findPrevious.value, getSearchOptions()); + } }); // fit is called within a setTimeout, cols and rows need this. diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index 76fe4bc9..2489844e 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -25,7 +25,10 @@ export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; - /** Assume caller implements 'search as you type' where findNext gets called when search input changes */ + /** + * Use this when you want the selection to expand if it still matches as the + * user types. Note that this only affects findNext. + */ incremental?: boolean; } diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 0a66fdff..8972917b 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -52,24 +52,27 @@ export class SearchHelper implements ISearchHelper { startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0]; } } - console.log(`Start from ${startCol},${startRow}`); this._initLinesCache(); - // Search from startRow to end - for (let y = startRow; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - result = this._findInLine(term, { row: y, col: startCol }, searchOptions); - if (result) { - break; + // Search startRow + result = this._findInLine(term, { row: startRow, col: startCol }, searchOptions); + + // Search from startRow + 1 to end + if (!result) { + for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + result = this._findInLine(term, { row: y, col: 0 }, searchOptions); + if (result) { + break; + } } - startCol = 0; } - // Search from the top to the startRow + // Search from the top to the startRow (search the whole startRow again in + // case startCol > 0) if (!result) { - for (let y = 0; y < startRow; y++) { - startCol = 0; - result = this._findInLine(term, {row: y, col: startCol}, searchOptions); + for (let y = 0; y <= startRow; y++) { + result = this._findInLine(term, {row: y, col: 0}, searchOptions); if (result) { break; } @@ -89,7 +92,6 @@ export class SearchHelper implements ISearchHelper { */ public findPrevious(term: string, searchOptions?: ISearchOptions): boolean { const selectionManager = this._terminal._core.selectionManager; - const {incremental} = searchOptions; let result: ISearchResult; if (!term || term.length === 0) { @@ -111,21 +113,31 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); - // Search from startRow to top - for (let y = startRow; y >= 0; y--) { - result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); - if (result) { - break; + // Search startRow + result = this._findInLine(term, { row: startRow, col: startCol }, searchOptions, isReverseSearch); + + // Search from startRow - 1 to top + if (!result) { + for (let y = startRow - 1; y >= 0; y--) { + result = this._findInLine(term, { + row: y, + col: this._terminal._core.buffer.lines.get(y).length + }, searchOptions, isReverseSearch); + if (result) { + break; + } } - startCol = y > 0 ? this._terminal._core.buffer.lines.get(y - 1).length : 0; } - // Search from the bottom to startRow + // Search from the bottom to startRow (search the whole startRow again in + // case startCol > 0) if (!result) { const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; - for (let y = searchFrom; y > startRow; y--) { - startCol = this._terminal._core.buffer.lines.get(y).length; - result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); + for (let y = searchFrom; y >= startRow; y--) { + result = this._findInLine(term, { + row: y, + col: this._terminal._core.buffer.lines.get(y).length + }, searchOptions, isReverseSearch); if (result) { break; } @@ -217,7 +229,6 @@ export class SearchHelper implements ISearchHelper { resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length); } else { resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col); - console.log('resultIndex', resultIndex); } } From 1a1f7e984fcda6ca4f565a717d8f5ed8fe0a993f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 16:03:31 -0800 Subject: [PATCH 45/51] Remove ISearchIndex object --- src/addons/search/Interfaces.ts | 7 ++--- src/addons/search/SearchHelper.ts | 49 ++++++++++++++----------------- src/addons/search/search.test.ts | 34 ++++++++++----------- 3 files changed, 41 insertions(+), 49 deletions(-) diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index 2489844e..a1f05895 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -32,11 +32,8 @@ export interface ISearchOptions { incremental?: boolean; } -export interface ISearchIndex { +export interface ISearchResult { + term: string; col: number; row: number; } - -export interface ISearchResult extends ISearchIndex { - term: string; -} diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 8972917b..756f2da7 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; +import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces'; const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs @@ -56,12 +56,12 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); // Search startRow - result = this._findInLine(term, { row: startRow, col: startCol }, searchOptions); + result = this._findInLine(term, startRow, startCol, searchOptions); // Search from startRow + 1 to end if (!result) { for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - result = this._findInLine(term, { row: y, col: 0 }, searchOptions); + result = this._findInLine(term, y, 0, searchOptions); if (result) { break; } @@ -72,7 +72,7 @@ export class SearchHelper implements ISearchHelper { // case startCol > 0) if (!result) { for (let y = 0; y <= startRow; y++) { - result = this._findInLine(term, {row: y, col: 0}, searchOptions); + result = this._findInLine(term, y, 0, searchOptions); if (result) { break; } @@ -114,15 +114,12 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); // Search startRow - result = this._findInLine(term, { row: startRow, col: startCol }, searchOptions, isReverseSearch); + result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch); // Search from startRow - 1 to top if (!result) { for (let y = startRow - 1; y >= 0; y--) { - result = this._findInLine(term, { - row: y, - col: this._terminal._core.buffer.lines.get(y).length - }, searchOptions, isReverseSearch); + result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch); if (result) { break; } @@ -134,10 +131,7 @@ export class SearchHelper implements ISearchHelper { if (!result) { const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; for (let y = searchFrom; y >= startRow; y--) { - result = this._findInLine(term, { - row: y, - col: this._terminal._core.buffer.lines.get(y).length - }, searchOptions, isReverseSearch); + result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch); if (result) { break; } @@ -187,20 +181,21 @@ export class SearchHelper implements ISearchHelper { * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the * text starts on is searched. * @param term The search term. - * @param y The line to search. + * @param row The line to start the search from. + * @param col The column to start the search from. * @param searchOptions Search options. * @return The search result if it was found. */ - protected _findInLine(term: string, searchIndex: ISearchIndex, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { - if (this._terminal._core.buffer.lines.get(searchIndex.row).isWrapped) { + protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { + if (this._terminal._core.buffer.lines.get(row).isWrapped) { return; } - let stringLine = this._linesCache ? this._linesCache[searchIndex.row] : void 0; + let stringLine = this._linesCache ? this._linesCache[row] : void 0; if (stringLine === void 0) { - stringLine = this.translateBufferLineToStringWithWrap(searchIndex.row, true); + stringLine = this.translateBufferLineToStringWithWrap(row, true); if (this._linesCache) { - this._linesCache[searchIndex.row] = stringLine; + this._linesCache[row] = stringLine; } } @@ -212,37 +207,37 @@ export class SearchHelper implements ISearchHelper { const searchRegex = RegExp(searchTerm, 'g'); let foundTerm: RegExpExecArray; if (isReverseSearch) { - while (foundTerm = searchRegex.exec(searchStringLine.slice(0, searchIndex.col))) { + while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) { resultIndex = searchRegex.lastIndex - foundTerm[0].length; term = foundTerm[0]; searchRegex.lastIndex -= (term.length - 1); } } else { - foundTerm = searchRegex.exec(searchStringLine.slice(searchIndex.col)); + foundTerm = searchRegex.exec(searchStringLine.slice(col)); if (foundTerm && foundTerm[0].length > 0) { - resultIndex = searchIndex.col + (searchRegex.lastIndex - foundTerm[0].length); + resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length); term = foundTerm[0]; } } } else { if (isReverseSearch) { - resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length); + resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); } else { - resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col); + resultIndex = searchStringLine.indexOf(searchTerm, col); } } if (resultIndex >= 0) { // Adjust the row number and search index if needed since a "line" of text can span multiple rows if (resultIndex >= this._terminal.cols) { - searchIndex.row += Math.floor(resultIndex / this._terminal.cols); + row += Math.floor(resultIndex / this._terminal.cols); resultIndex = resultIndex % this._terminal.cols; } if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) { return; } - const line = this._terminal._core.buffer.lines.get(searchIndex.row); + const line = this._terminal._core.buffer.lines.get(row); for (let i = 0; i < resultIndex; i++) { const charData = line.get(i); @@ -261,7 +256,7 @@ export class SearchHelper implements ISearchHelper { return { term, col: resultIndex, - row: searchIndex.row + row }; } } diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index e9c11fb2..6551fa8b 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -7,7 +7,7 @@ declare var require: any; import { assert, expect } from 'chai'; import * as search from './search'; import { SearchHelper } from './SearchHelper'; -import { ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; +import { ISearchOptions, ISearchResult } from './Interfaces'; class MockTerminalPlain {} @@ -30,10 +30,10 @@ class MockTerminal { class TestSearchHelper extends SearchHelper { public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult { - return this._findInLine(term, {row: rowNumber, col: 0}, searchOptions); + return this._findInLine(term, rowNumber, 0, searchOptions); } - public findFromIndex(term: string, searchIndex: ISearchIndex, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult { - return this._findInLine(term, searchIndex, searchOptions, isReverseSearch); + public findFromIndex(term: string, row: number, col: number, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult { + return this._findInLine(term, row, col, searchOptions, isReverseSearch); } } @@ -258,11 +258,11 @@ describe('search addon', () => { wholeWord: false, caseSensitive: false }; - const find0 = term.searchHelper.findFromIndex('hello', {row: 0, col: 0}, searchOptions); - const find1 = term.searchHelper.findFromIndex('hello', {row: 0, col: find0.col + find0.term.length}, searchOptions); - const find2 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: 0}, searchOptions); - const find3 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: find2.col + find2.term.length}, searchOptions); - const find4 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: find3.col + find3.term.length}, searchOptions); + const find0 = term.searchHelper.findFromIndex('hello', 0, 0, searchOptions); + const find1 = term.searchHelper.findFromIndex('hello', 0, find0.col + find0.term.length, searchOptions); + const find2 = term.searchHelper.findFromIndex('aaaa', 1, 0, searchOptions); + const find3 = term.searchHelper.findFromIndex('aaaa', 1, find2.col + find2.term.length, searchOptions); + const find4 = term.searchHelper.findFromIndex('aaaa', 1, find3.col + find3.term.length, searchOptions); expect(find0).eql({col: 0, row: 0, term: 'hello'}); expect(find1).eql({col: 9, row: 0, term: 'hello'}); expect(find2).eql({col: 0, row: 1, term: 'aaaa'}); @@ -280,10 +280,10 @@ describe('search addon', () => { caseSensitive: false }; const isReverseSearch = true; - const find0 = term.searchHelper.findFromIndex('is', {row: 0, col: 16}, searchOptions, isReverseSearch); - const find1 = term.searchHelper.findFromIndex('is', {row: 0, col: find0.col}, searchOptions, isReverseSearch); - const find2 = term.searchHelper.findFromIndex('it', {row: 0, col: 16}, searchOptions, isReverseSearch); - const find3 = term.searchHelper.findFromIndex('it', {row: 0, col: find2.col}, searchOptions, isReverseSearch); + const find0 = term.searchHelper.findFromIndex('is', 0, 16, searchOptions, isReverseSearch); + const find1 = term.searchHelper.findFromIndex('is', 0, find0.col, searchOptions, isReverseSearch); + const find2 = term.searchHelper.findFromIndex('it', 0, 16, searchOptions, isReverseSearch); + const find3 = term.searchHelper.findFromIndex('it', 0, find2.col, searchOptions, isReverseSearch); expect(find0).eql({col: 14, row: 0, term: 'is'}); expect(find1).eql({col: 3, row: 0, term: 'is'}); expect(find2).eql({col: 11, row: 0, term: 'it'}); @@ -300,10 +300,10 @@ describe('search addon', () => { caseSensitive: true }; const isReverseSearch = true; - const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: 16}, searchOptions, isReverseSearch); - const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find0.col}, searchOptions, isReverseSearch); - const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find1.col}, searchOptions, isReverseSearch); - const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find2.col}, searchOptions, isReverseSearch); + const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, 16, searchOptions, isReverseSearch); + const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find0.col, searchOptions, isReverseSearch); + const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find1.col, searchOptions, isReverseSearch); + const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find2.col, searchOptions, isReverseSearch); expect(find0).eql({col: 13, row: 0, term: 'ABC'}); expect(find1).eql({col: 10, row: 0, term: 'ABC'}); expect(find2).eql({col: 3, row: 0, term: 'ABC'}); From d698faa18366df2592f5992ace5517d366382f79 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 16:12:53 -0800 Subject: [PATCH 46/51] Comment how the regex reverse search while works --- src/addons/search/SearchHelper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 756f2da7..42a3a122 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -207,6 +207,7 @@ export class SearchHelper implements ISearchHelper { const searchRegex = RegExp(searchTerm, 'g'); let foundTerm: RegExpExecArray; if (isReverseSearch) { + // This loop will get the resultIndex of the _last_ regex match in the range 0..col while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) { resultIndex = searchRegex.lastIndex - foundTerm[0].length; term = foundTerm[0]; From 9bcfb4d7225e178a93389b96c9b1225e85f74182 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 10:04:48 -0800 Subject: [PATCH 47/51] Tidy up wrapping style --- src/core/input/Keyboard.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 37e663f5..9d86b349 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,8 +349,9 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } - } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 - && ev.keyCode !== 144 && ev.keyCode !== 145) { // Include only keys that that result in a character; don't include num lock and scroll lock + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && + ev.keyCode >= 48 && ev.keyCode !== 144 && ev.keyCode !== 145) { + // Include only keys that that result in a character; don't include num lock and scroll lock result.key = ev.key; } break; From 51e1f49bf36085e46d58ce98c07e115d74c112a8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 10:26:40 -0800 Subject: [PATCH 48/51] Make dispose more resilient --- src/EscapeSequenceParser.test.ts | 24 ++++++++++++++++++++++-- src/EscapeSequenceParser.ts | 20 ++++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 9216dd4c..0f1d63cc 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1216,7 +1216,7 @@ describe('EscapeSequenceParser', function (): void { parser2.parse('\x1b[0m'); chai.expect(order).eql([3, 2, 1]); }); - it('Dispose', () => { + it('Dispose should work', () => { const csiCustom: [string, number[], string][] = []; parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); @@ -1225,6 +1225,16 @@ describe('EscapeSequenceParser', function (): void { chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); }); + it('Should not corrupt the parser when dispose is called twice', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + customHandler.dispose(); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + }); }); it('EXECUTE handler', function (): void { parser2.setExecuteHandler('\n', function (): void { @@ -1300,7 +1310,7 @@ describe('EscapeSequenceParser', function (): void { parser2.parse('\x1b]1;foo=bar\x1b\\'); chai.expect(order).eql([3, 2, 1]); }); - it('Dispose', () => { + it('Dispose should work', () => { const oscCustom: [number, string][] = []; parser2.setOscHandler(1, data => osc.push([1, data])); const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); @@ -1309,6 +1319,16 @@ describe('EscapeSequenceParser', function (): void { chai.expect(osc).eql([[1, 'foo=bar']]); chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); }); + it('Should not corrupt the parser when dispose is called twice', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + customHandler.dispose(); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + }); }); it('DCS handler', function (): void { parser2.setDcsHandler('+p', { diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index acb5a42d..70c1c6c8 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -316,9 +316,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (this._csiHandlers[index] === undefined) { this._csiHandlers[index] = []; } - this._csiHandlers[index].push(callback); + const handlerList = this._csiHandlers[index]; + handlerList.push(callback); return { - dispose: () => this._csiHandlers[index].splice(this._csiHandlers[index].indexOf(callback)) + dispose: () => { + const handlerIndex = handlerList.indexOf(callback); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex); + } + } }; } setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { @@ -345,9 +351,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (this._oscHandlers[ident] === undefined) { this._oscHandlers[ident] = []; } - this._oscHandlers[ident].push(callback); + const handlerList = this._oscHandlers[ident]; + handlerList.push(callback); return { - dispose: () => this._oscHandlers[ident].splice(this._oscHandlers[ident].indexOf(callback)) + dispose: () => { + const handlerIndex = handlerList.indexOf(callback); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex); + } + } }; } setOscHandler(ident: number, callback: (data: string) => void): void { From bb198a980cde892c247e403b435535e2a676eee9 Mon Sep 17 00:00:00 2001 From: Vincent Woo Date: Thu, 27 Dec 2018 13:54:11 -0800 Subject: [PATCH 49/51] Small indent fix --- src/ui/CharMeasure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/CharMeasure.ts b/src/ui/CharMeasure.ts index 7d1e5e48..2dfc4eb5 100644 --- a/src/ui/CharMeasure.ts +++ b/src/ui/CharMeasure.ts @@ -38,7 +38,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { } public measure(options: ITerminalOptions): void { - this._measureElement.style.fontFamily = options.fontFamily; + this._measureElement.style.fontFamily = options.fontFamily; this._measureElement.style.fontSize = `${options.fontSize}px`; const geometry = this._measureElement.getBoundingClientRect(); // The element is likely currently display:none, we should retain the From 913150fe13ff45379accd92d67dc71ebf54d4f98 Mon Sep 17 00:00:00 2001 From: ntchjb Date: Sat, 29 Dec 2018 03:03:23 +0700 Subject: [PATCH 50/51] Fix search addons: searchTerm should not be matched if the beginning of matching index < 0 --- src/addons/search/SearchHelper.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 42a3a122..96cd845d 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -222,7 +222,9 @@ export class SearchHelper implements ISearchHelper { } } else { if (isReverseSearch) { - resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); + if (col - searchTerm.length >= 0) { + resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); + } } else { resultIndex = searchStringLine.indexOf(searchTerm, col); } From 8fbeadd5f9f1b3c3eff1a82ce869d1e0c9e1a2f8 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 1 Jan 2019 09:45:23 -0800 Subject: [PATCH 51/51] Add missing deleteCount argument to Array.splice calls. --- src/EscapeSequenceParser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 70c1c6c8..6d3de0e0 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -322,7 +322,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP dispose: () => { const handlerIndex = handlerList.indexOf(callback); if (handlerIndex !== -1) { - handlerList.splice(handlerIndex); + handlerList.splice(handlerIndex, 1); } } }; @@ -357,7 +357,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP dispose: () => { const handlerIndex = handlerList.indexOf(callback); if (handlerIndex !== -1) { - handlerList.splice(handlerIndex); + handlerList.splice(handlerIndex, 1); } } };