From 565cba9c1ae6393c482b64e9a7a99bfa06e7776e Mon Sep 17 00:00:00 2001 From: ntchjb Date: Sat, 29 Dec 2018 09:38:17 +0700 Subject: [PATCH 01/40] Fix search addons: - Changed to use `this._terminal.cols` for buffer line length instead. - Let `_findInLine` check on wrapped line - Added conditions in `_findInLine` - For reverse search, if there is no selection at given row, then start scan at the end of the string --- src/addons/search/SearchHelper.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 96cd845d..3537295d 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -101,7 +101,7 @@ export class SearchHelper implements ISearchHelper { const isReverseSearch = true; let startRow = this._terminal._core.buffer.ydisp; - let startCol: number = this._terminal._core.buffer.lines.get(startRow).length; + let startCol: number = this._terminal.cols; if (selectionManager.selectionStart) { // Start from the selection start if there is a selection @@ -119,7 +119,7 @@ export class SearchHelper implements ISearchHelper { // Search from startRow - 1 to top if (!result) { for (let y = startRow - 1; y >= 0; y--) { - result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch); + result = this._findInLine(term, y, this._terminal.cols, searchOptions, isReverseSearch); if (result) { break; } @@ -131,7 +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, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch); + result = this._findInLine(term, y, this._terminal.cols, searchOptions, isReverseSearch); if (result) { break; } @@ -187,9 +187,6 @@ export class SearchHelper implements ISearchHelper { * @return The search result if it was found. */ 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[row] : void 0; if (stringLine === void 0) { @@ -222,7 +219,12 @@ export class SearchHelper implements ISearchHelper { } } else { if (isReverseSearch) { - if (col - searchTerm.length >= 0) { + // If the given row has no selection (col is equal to row length), + // lastIndexOf needs to scan at the end of the searchStringLine + if (col === this._terminal.cols) { + resultIndex = searchStringLine.lastIndexOf(searchTerm, col - 1); + } + else if (col - searchTerm.length >= 0) { resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); } } else { From d94c89775421e5c98ccf8552c5ef373e9ca7ceb3 Mon Sep 17 00:00:00 2001 From: ntchjb Date: Sat, 29 Dec 2018 13:55:14 +0700 Subject: [PATCH 02/40] Fix search addons: be able to search wrapped lines from unwrapped line by managing column range that is needed to be scanned. --- src/addons/search/SearchHelper.ts | 70 ++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 3537295d..7200539b 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -60,22 +60,54 @@ export class SearchHelper implements ISearchHelper { // Search from startRow + 1 to end if (!result) { + // A row that has isWrapped = false + let findingRow = startRow; + // index of beginning column that _findInLine need to scan. + let cumulativeCols = startCol; + // If startRow is wrapped row, scan for unwrapped row above. + // So we can start matching on wrapped line from long unwrapped line. + while (this._terminal._core.buffer.lines.get(findingRow).isWrapped) { + findingRow--; + cumulativeCols += this._terminal.cols; + } + for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - result = this._findInLine(term, y, 0, searchOptions); + // Run _findInLine at unwrapped row, scan for cumulativeCols columns + result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); if (result) { break; } + // If the current line is wrapped line, increase index of column to ignore the previous scan + // Otherwise, reset beginning column index to zero with set new unwrapped line index + if (this._terminal._core.buffer.lines.get(y).isWrapped) { + cumulativeCols += this._terminal.cols; + } else { + cumulativeCols = 0; + findingRow = y; + } } } // Search from the top to the startRow (search the whole startRow again in // case startCol > 0) if (!result) { + // Assume that The first line is always unwrapped line + let findingRow = 0; + // Scan at beginning of the line + let cumulativeCols = 0; for (let y = 0; y <= startRow; y++) { - result = this._findInLine(term, y, 0, searchOptions); + result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); if (result) { break; } + // If the current line is wrapped line, increase index of beginning column + // So we ignore the previous scan + if (this._terminal._core.buffer.lines.get(y).isWrapped) { + cumulativeCols += this._terminal.cols; + } else { + cumulativeCols = 0; + findingRow = y; + } } } @@ -118,11 +150,24 @@ export class SearchHelper implements ISearchHelper { // Search from startRow - 1 to top if (!result) { + // If the line is wrapped line, increase number of columns that is needed to be scanned + // Se we can scan on wrapped line from unwrapped line + let cumulativeCols = this._terminal.cols; + if (this._terminal._core.buffer.lines.get(startRow).isWrapped) { + cumulativeCols += startCol; + } for (let y = startRow - 1; y >= 0; y--) { - result = this._findInLine(term, y, this._terminal.cols, searchOptions, isReverseSearch); + result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch); if (result) { break; } + // If the current line is wrapped line, increase scanning range, + // preparing for scanning on unwrapped line + if (this._terminal._core.buffer.lines.get(y).isWrapped) { + cumulativeCols += this._terminal.cols; + } else { + cumulativeCols = this._terminal.cols; + } } } @@ -130,11 +175,17 @@ export class SearchHelper implements ISearchHelper { // case startCol > 0) if (!result) { const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; + let cumulativeCols = this._terminal.cols; for (let y = searchFrom; y >= startRow; y--) { - result = this._findInLine(term, y, this._terminal.cols, searchOptions, isReverseSearch); + result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch); if (result) { break; } + if (this._terminal._core.buffer.lines.get(y).isWrapped) { + cumulativeCols += this._terminal.cols; + } else { + cumulativeCols = this._terminal.cols; + } } } @@ -188,6 +239,10 @@ export class SearchHelper implements ISearchHelper { */ protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { + // Ignore wrapped lines, only consider on unwrapped line (first row of command string). + if (this._terminal._core.buffer.lines.get(row).isWrapped) { + return; + } let stringLine = this._linesCache ? this._linesCache[row] : void 0; if (stringLine === void 0) { stringLine = this.translateBufferLineToStringWithWrap(row, true); @@ -219,12 +274,7 @@ export class SearchHelper implements ISearchHelper { } } else { if (isReverseSearch) { - // If the given row has no selection (col is equal to row length), - // lastIndexOf needs to scan at the end of the searchStringLine - if (col === this._terminal.cols) { - resultIndex = searchStringLine.lastIndexOf(searchTerm, col - 1); - } - else if (col - searchTerm.length >= 0) { + if (col - searchTerm.length >= 0) { resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); } } else { From f961f90bc83ab249529f4187ecb250168176bfbf Mon Sep 17 00:00:00 2001 From: ntchjb Date: Tue, 1 Jan 2019 16:39:23 +0700 Subject: [PATCH 03/40] Support wide characters and combined characters - Convert buffer index to string index before searching - Convert string index to buffer index after searching - Fix bug when search selection skipped some result --- src/addons/search/SearchHelper.ts | 214 +++++++++++++++++++++++------- 1 file changed, 164 insertions(+), 50 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 7200539b..69fd919f 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -7,6 +7,8 @@ import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } fr const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs +const CHAR_DATA_CHAR_INDEX = 1; +const CHAR_DATA_WIDTH_INDEX = 2; /** * A class that knows how to search the terminal and how to display the results. @@ -55,28 +57,25 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); - // Search startRow - result = this._findInLine(term, startRow, startCol, searchOptions); + // The row that has isWrapped = false + let findingRow = startRow; + // index of beginning column that _findInLine need to scan. + let cumulativeCols = startCol; + // If startRow is wrapped row, scan for unwrapped row above. + // So we can start matching on wrapped line from long unwrapped line. + while (this._terminal._core.buffer.lines.get(findingRow).isWrapped) { + findingRow--; + cumulativeCols += this._terminal.cols; + } - // Search from startRow + 1 to end + // Search unwarpped row + result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); + + // Search from startRow + 1 to end, if the row is still wrapped line, increase cumulativeCols, + // otherwise, reset it and set the new unwrapped line index. if (!result) { - // A row that has isWrapped = false - let findingRow = startRow; - // index of beginning column that _findInLine need to scan. - let cumulativeCols = startCol; - // If startRow is wrapped row, scan for unwrapped row above. - // So we can start matching on wrapped line from long unwrapped line. - while (this._terminal._core.buffer.lines.get(findingRow).isWrapped) { - findingRow--; - cumulativeCols += this._terminal.cols; - } for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - // Run _findInLine at unwrapped row, scan for cumulativeCols columns - result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); - if (result) { - break; - } // If the current line is wrapped line, increase index of column to ignore the previous scan // Otherwise, reset beginning column index to zero with set new unwrapped line index if (this._terminal._core.buffer.lines.get(y).isWrapped) { @@ -85,6 +84,11 @@ export class SearchHelper implements ISearchHelper { cumulativeCols = 0; findingRow = y; } + // Run _findInLine at unwrapped row, start scan at cumulativeCols column index + result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); + if (result) { + break; + } } } @@ -226,11 +230,129 @@ export class SearchHelper implements ISearchHelper { (((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1))); } + /** + * Translates a string index back to a BufferIndex. + * To get the correct buffer position the string must start at `startCol` 0 + * (default in translateBufferLineToString). + * This method is similar to stringIndexToBufferIndex in Buffer.ts + * but this method added some modification that, if it found an empty cell, + * the method will see it as a whitespace and count it as a character. + * The modification is added because the given string index may include + * empty cells inside the string, which is needed to be counted. + * The return value of this method is the same as BufferIndex + * @param lineIndex line index the string was retrieved from + * @param stringIndex index within the string + * @param startCol column offset the string was retrieved from + */ + private _stringIndexToBufferIndex(lineIndex: number, stringIndex: number): [number, number] { + while (stringIndex) { + const line = this._terminal._core.buffer.lines.get(lineIndex); + if (!line) { + return [-1, -1]; + } + for (let i = 0; i < this._terminal.cols; ++i) { + const charData = line.get(i); + const char = charData[CHAR_DATA_CHAR_INDEX]; + // If found empty cell with width equals to 1, see it as whitespace + if (charData[CHAR_DATA_CHAR_INDEX] === '' && charData[CHAR_DATA_WIDTH_INDEX] > 0) { + stringIndex--; + } + stringIndex -= char.length; + if (stringIndex < 0) { + return [lineIndex, i]; + } + } + lineIndex++; + } + return [lineIndex, 0]; + } + + /** + * Convert buffer index of unwrapped row to string index. + * @param lineIndex index of terminal row that is unwrapped + * @param bufferIndex index of terminal column on unwrapped row + */ + private _bufferIndexToStringIndex(lineIndex: number, bufferIndex: number): number { + let stringIndex = -1; + const buffer = this._terminal._core.buffer; + while (bufferIndex >= 0) { + const line = buffer.lines.get(lineIndex); + // Exceed index of bottom row, returned + if (!line) { + break; + } + + let lineLength = this._terminal.cols; + + // At the last line, lineLength will be trimmed to remove trailing empty cells + if (bufferIndex < lineLength) { + // Add 1 to getTrimmedLength because if providing bufferIndex is larger than + // converted string length, the result should be `string length`, not `string length - 1` + // to make sure that searching range includes the last character in the string + lineLength = line.getTrimmedLength() + 1; + } + + for (let i = 0; i < lineLength; i++) { + const cell = line.get(i); + + // Count number of characters from current buffer column in each cell. + stringIndex += cell[CHAR_DATA_CHAR_INDEX].length; + bufferIndex--; + + // If found empty cell, act like found whitespace + if (cell[CHAR_DATA_CHAR_INDEX] === '' && cell[CHAR_DATA_WIDTH_INDEX] > 0) { + stringIndex++; + } + + if (bufferIndex < 0) { + return stringIndex; + } + } + lineIndex++; + } + return stringIndex; + } + + /** + * Get buffer length (number of cells) from provided string + * @param result The search result object including term, row, and col + */ + private _getCellLengthFromString(result: ISearchResult): number { + const length = result.term.length; + + let strCount = 0; + let cellCount = 0; + let { col, row } = result; + let rowContent = this._terminal._core.buffer.lines.get(row); + + // Count cells along with characters until the number of characters + // exceeds string length of search result. + while (strCount <= length) { + strCount += rowContent.get(col)[CHAR_DATA_CHAR_INDEX].length; + cellCount += rowContent.get(col)[CHAR_DATA_WIDTH_INDEX]; + if (strCount >= length) { + break; + } + col++; + + // In case that current cell exceed total number of cells in a row + // Begin col at 0 on the next line + if (col >= this._terminal.cols) { + col = 0; + rowContent = this._terminal._core.buffer.lines.get(++row); + } + } + return cellCount; + } /** * Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain * subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that * 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. + * + * The concept of searching is that: + * Get unwarpped line as string => convert rowand col to string index => begin searching to get search result as + * string index => convert back to buffer index (col, row) => return the result. * @param term The search term. * @param row The line to start the search from. * @param col The column to start the search from. @@ -243,6 +365,8 @@ export class SearchHelper implements ISearchHelper { if (this._terminal._core.buffer.lines.get(row).isWrapped) { return; } + + // Get unwrapped string from buffer lines let stringLine = this._linesCache ? this._linesCache[row] : void 0; if (stringLine === void 0) { stringLine = this.translateBufferLineToStringWithWrap(row, true); @@ -251,67 +375,54 @@ export class SearchHelper implements ISearchHelper { } } + // Check for case sensitive option const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); let resultIndex = -1; + // Convert from buffer index (col, row) to string index before begin searching + const stringIndex = this._bufferIndexToStringIndex(row, col); + if (searchOptions.regex) { 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))) { + while (foundTerm = searchRegex.exec(searchStringLine.slice(0, stringIndex))) { resultIndex = searchRegex.lastIndex - foundTerm[0].length; term = foundTerm[0]; searchRegex.lastIndex -= (term.length - 1); } } else { - foundTerm = searchRegex.exec(searchStringLine.slice(col)); + foundTerm = searchRegex.exec(searchStringLine.slice(stringIndex)); if (foundTerm && foundTerm[0].length > 0) { - resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length); + resultIndex = stringIndex + (searchRegex.lastIndex - foundTerm[0].length); term = foundTerm[0]; } } } else { if (isReverseSearch) { - if (col - searchTerm.length >= 0) { - resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); + if (stringIndex - searchTerm.length >= 0) { + resultIndex = searchStringLine.lastIndexOf(searchTerm, stringIndex - searchTerm.length); } } else { - resultIndex = searchStringLine.indexOf(searchTerm, col); + resultIndex = searchStringLine.indexOf(searchTerm, stringIndex); } } 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) { - row += Math.floor(resultIndex / this._terminal.cols); - resultIndex = resultIndex % this._terminal.cols; - } + // After getting the result as string index, convert it to buffer index. + const resultBufferIndex = this._stringIndexToBufferIndex(row, resultIndex); + + // Check for wholeword option if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) { return; } - const line = this._terminal._core.buffer.lines.get(row); - - 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) { - 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) { - resultIndex++; - } - } return { term, - col: resultIndex, - row + col: resultBufferIndex[1], + row: resultBufferIndex[0] }; } } @@ -321,7 +432,7 @@ export class SearchHelper implements ISearchHelper { * function is useful for getting the actual text underneath the raw selection * position. * @param line The line being translated. - * @param trimRight Whether to trim whitespace to the right. + * @param trimRight Whether to trim -space to the right. */ public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string { let lineString = ''; @@ -330,7 +441,8 @@ export class SearchHelper implements ISearchHelper { do { const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1); lineWrapsToNext = nextLine ? nextLine.isWrapped : false; - lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._terminal.cols); + // string should be cut with string index, not buffer index to support wide characters + lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._bufferIndexToStringIndex(lineIndex, this._terminal.cols)); lineIndex++; } while (lineWrapsToNext); @@ -347,7 +459,9 @@ export class SearchHelper implements ISearchHelper { this._terminal.clearSelection(); return false; } - this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length); + // The selection length should be number of cell needed to be selected, not string length. + // To support wide character + this._terminal._core.selectionManager.setSelection(result.col, result.row, this._getCellLengthFromString(result)); this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp); return true; } From efe22d8cf3e527fc55090bff175158b938b9eeb3 Mon Sep 17 00:00:00 2001 From: ntchjb Date: Thu, 3 Jan 2019 15:32:25 +0700 Subject: [PATCH 04/40] Revert "Support wide characters and combined characters" This reverts commit f961f90bc83ab249529f4187ecb250168176bfbf. --- src/addons/search/SearchHelper.ts | 214 +++++++----------------------- 1 file changed, 50 insertions(+), 164 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 69fd919f..7200539b 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -7,8 +7,6 @@ import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } fr const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs -const CHAR_DATA_CHAR_INDEX = 1; -const CHAR_DATA_WIDTH_INDEX = 2; /** * A class that knows how to search the terminal and how to display the results. @@ -57,25 +55,28 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); - // The row that has isWrapped = false - let findingRow = startRow; - // index of beginning column that _findInLine need to scan. - let cumulativeCols = startCol; - // If startRow is wrapped row, scan for unwrapped row above. - // So we can start matching on wrapped line from long unwrapped line. - while (this._terminal._core.buffer.lines.get(findingRow).isWrapped) { - findingRow--; - cumulativeCols += this._terminal.cols; - } + // Search startRow + result = this._findInLine(term, startRow, startCol, searchOptions); - // Search unwarpped row - result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); - - // Search from startRow + 1 to end, if the row is still wrapped line, increase cumulativeCols, - // otherwise, reset it and set the new unwrapped line index. + // Search from startRow + 1 to end if (!result) { + // A row that has isWrapped = false + let findingRow = startRow; + // index of beginning column that _findInLine need to scan. + let cumulativeCols = startCol; + // If startRow is wrapped row, scan for unwrapped row above. + // So we can start matching on wrapped line from long unwrapped line. + while (this._terminal._core.buffer.lines.get(findingRow).isWrapped) { + findingRow--; + cumulativeCols += this._terminal.cols; + } for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + // Run _findInLine at unwrapped row, scan for cumulativeCols columns + result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); + if (result) { + break; + } // If the current line is wrapped line, increase index of column to ignore the previous scan // Otherwise, reset beginning column index to zero with set new unwrapped line index if (this._terminal._core.buffer.lines.get(y).isWrapped) { @@ -84,11 +85,6 @@ export class SearchHelper implements ISearchHelper { cumulativeCols = 0; findingRow = y; } - // Run _findInLine at unwrapped row, start scan at cumulativeCols column index - result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); - if (result) { - break; - } } } @@ -230,129 +226,11 @@ export class SearchHelper implements ISearchHelper { (((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1))); } - /** - * Translates a string index back to a BufferIndex. - * To get the correct buffer position the string must start at `startCol` 0 - * (default in translateBufferLineToString). - * This method is similar to stringIndexToBufferIndex in Buffer.ts - * but this method added some modification that, if it found an empty cell, - * the method will see it as a whitespace and count it as a character. - * The modification is added because the given string index may include - * empty cells inside the string, which is needed to be counted. - * The return value of this method is the same as BufferIndex - * @param lineIndex line index the string was retrieved from - * @param stringIndex index within the string - * @param startCol column offset the string was retrieved from - */ - private _stringIndexToBufferIndex(lineIndex: number, stringIndex: number): [number, number] { - while (stringIndex) { - const line = this._terminal._core.buffer.lines.get(lineIndex); - if (!line) { - return [-1, -1]; - } - for (let i = 0; i < this._terminal.cols; ++i) { - const charData = line.get(i); - const char = charData[CHAR_DATA_CHAR_INDEX]; - // If found empty cell with width equals to 1, see it as whitespace - if (charData[CHAR_DATA_CHAR_INDEX] === '' && charData[CHAR_DATA_WIDTH_INDEX] > 0) { - stringIndex--; - } - stringIndex -= char.length; - if (stringIndex < 0) { - return [lineIndex, i]; - } - } - lineIndex++; - } - return [lineIndex, 0]; - } - - /** - * Convert buffer index of unwrapped row to string index. - * @param lineIndex index of terminal row that is unwrapped - * @param bufferIndex index of terminal column on unwrapped row - */ - private _bufferIndexToStringIndex(lineIndex: number, bufferIndex: number): number { - let stringIndex = -1; - const buffer = this._terminal._core.buffer; - while (bufferIndex >= 0) { - const line = buffer.lines.get(lineIndex); - // Exceed index of bottom row, returned - if (!line) { - break; - } - - let lineLength = this._terminal.cols; - - // At the last line, lineLength will be trimmed to remove trailing empty cells - if (bufferIndex < lineLength) { - // Add 1 to getTrimmedLength because if providing bufferIndex is larger than - // converted string length, the result should be `string length`, not `string length - 1` - // to make sure that searching range includes the last character in the string - lineLength = line.getTrimmedLength() + 1; - } - - for (let i = 0; i < lineLength; i++) { - const cell = line.get(i); - - // Count number of characters from current buffer column in each cell. - stringIndex += cell[CHAR_DATA_CHAR_INDEX].length; - bufferIndex--; - - // If found empty cell, act like found whitespace - if (cell[CHAR_DATA_CHAR_INDEX] === '' && cell[CHAR_DATA_WIDTH_INDEX] > 0) { - stringIndex++; - } - - if (bufferIndex < 0) { - return stringIndex; - } - } - lineIndex++; - } - return stringIndex; - } - - /** - * Get buffer length (number of cells) from provided string - * @param result The search result object including term, row, and col - */ - private _getCellLengthFromString(result: ISearchResult): number { - const length = result.term.length; - - let strCount = 0; - let cellCount = 0; - let { col, row } = result; - let rowContent = this._terminal._core.buffer.lines.get(row); - - // Count cells along with characters until the number of characters - // exceeds string length of search result. - while (strCount <= length) { - strCount += rowContent.get(col)[CHAR_DATA_CHAR_INDEX].length; - cellCount += rowContent.get(col)[CHAR_DATA_WIDTH_INDEX]; - if (strCount >= length) { - break; - } - col++; - - // In case that current cell exceed total number of cells in a row - // Begin col at 0 on the next line - if (col >= this._terminal.cols) { - col = 0; - rowContent = this._terminal._core.buffer.lines.get(++row); - } - } - return cellCount; - } /** * Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain * subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that * 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. - * - * The concept of searching is that: - * Get unwarpped line as string => convert rowand col to string index => begin searching to get search result as - * string index => convert back to buffer index (col, row) => return the result. * @param term The search term. * @param row The line to start the search from. * @param col The column to start the search from. @@ -365,8 +243,6 @@ export class SearchHelper implements ISearchHelper { if (this._terminal._core.buffer.lines.get(row).isWrapped) { return; } - - // Get unwrapped string from buffer lines let stringLine = this._linesCache ? this._linesCache[row] : void 0; if (stringLine === void 0) { stringLine = this.translateBufferLineToStringWithWrap(row, true); @@ -375,54 +251,67 @@ export class SearchHelper implements ISearchHelper { } } - // Check for case sensitive option const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); let resultIndex = -1; - // Convert from buffer index (col, row) to string index before begin searching - const stringIndex = this._bufferIndexToStringIndex(row, col); - if (searchOptions.regex) { 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, stringIndex))) { + 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(stringIndex)); + foundTerm = searchRegex.exec(searchStringLine.slice(col)); if (foundTerm && foundTerm[0].length > 0) { - resultIndex = stringIndex + (searchRegex.lastIndex - foundTerm[0].length); + resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length); term = foundTerm[0]; } } } else { if (isReverseSearch) { - if (stringIndex - searchTerm.length >= 0) { - resultIndex = searchStringLine.lastIndexOf(searchTerm, stringIndex - searchTerm.length); + if (col - searchTerm.length >= 0) { + resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); } } else { - resultIndex = searchStringLine.indexOf(searchTerm, stringIndex); + resultIndex = searchStringLine.indexOf(searchTerm, col); } } if (resultIndex >= 0) { - // After getting the result as string index, convert it to buffer index. - const resultBufferIndex = this._stringIndexToBufferIndex(row, resultIndex); - - // Check for wholeword option + // Adjust the row number and search index if needed since a "line" of text can span multiple rows + if (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(row); + + 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) { + 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) { + resultIndex++; + } + } return { term, - col: resultBufferIndex[1], - row: resultBufferIndex[0] + col: resultIndex, + row }; } } @@ -432,7 +321,7 @@ export class SearchHelper implements ISearchHelper { * function is useful for getting the actual text underneath the raw selection * position. * @param line The line being translated. - * @param trimRight Whether to trim -space to the right. + * @param trimRight Whether to trim whitespace to the right. */ public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string { let lineString = ''; @@ -441,8 +330,7 @@ export class SearchHelper implements ISearchHelper { do { const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1); lineWrapsToNext = nextLine ? nextLine.isWrapped : false; - // string should be cut with string index, not buffer index to support wide characters - lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._bufferIndexToStringIndex(lineIndex, this._terminal.cols)); + lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._terminal.cols); lineIndex++; } while (lineWrapsToNext); @@ -459,9 +347,7 @@ export class SearchHelper implements ISearchHelper { this._terminal.clearSelection(); return false; } - // The selection length should be number of cell needed to be selected, not string length. - // To support wide character - this._terminal._core.selectionManager.setSelection(result.col, result.row, this._getCellLengthFromString(result)); + this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length); this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp); return true; } From c784a012c79783084b9fa4b604f676a5f1cfd3dc Mon Sep 17 00:00:00 2001 From: ntchjb Date: Thu, 3 Jan 2019 15:55:27 +0700 Subject: [PATCH 05/40] Fix: update findingRow and cumulativeCols before running _findInLine to be able to scan unwrapped line at the last row --- src/addons/search/SearchHelper.ts | 35 +++++++++++++++++-------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 7200539b..0b7e8960 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -55,28 +55,25 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); + // A row that has isWrapped = false + let findingRow = startRow; + // index of beginning column that _findInLine need to scan. + let cumulativeCols = startCol; + // If startRow is wrapped row, scan for unwrapped row above. + // So we can start matching on wrapped line from long unwrapped line. + while (this._terminal._core.buffer.lines.get(findingRow).isWrapped) { + findingRow--; + cumulativeCols += this._terminal.cols; + } + // Search startRow - result = this._findInLine(term, startRow, startCol, searchOptions); + result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); // Search from startRow + 1 to end if (!result) { - // A row that has isWrapped = false - let findingRow = startRow; - // index of beginning column that _findInLine need to scan. - let cumulativeCols = startCol; - // If startRow is wrapped row, scan for unwrapped row above. - // So we can start matching on wrapped line from long unwrapped line. - while (this._terminal._core.buffer.lines.get(findingRow).isWrapped) { - findingRow--; - cumulativeCols += this._terminal.cols; - } for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - // Run _findInLine at unwrapped row, scan for cumulativeCols columns - result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); - if (result) { - break; - } + // If the current line is wrapped line, increase index of column to ignore the previous scan // Otherwise, reset beginning column index to zero with set new unwrapped line index if (this._terminal._core.buffer.lines.get(y).isWrapped) { @@ -85,6 +82,12 @@ export class SearchHelper implements ISearchHelper { cumulativeCols = 0; findingRow = y; } + + // Run _findInLine at unwrapped row, scan for cumulativeCols columns + result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); + if (result) { + break; + } } } From 92369345867d2e6f79722fcf5bac82f5a4e51d2d Mon Sep 17 00:00:00 2001 From: ntchjb Date: Wed, 9 Jan 2019 07:01:34 +0700 Subject: [PATCH 06/40] _findInLine() function should not run multiple times on the same row --- src/addons/search/SearchHelper.ts | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 0b7e8960..525e25dc 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -76,15 +76,7 @@ export class SearchHelper implements ISearchHelper { // If the current line is wrapped line, increase index of column to ignore the previous scan // Otherwise, reset beginning column index to zero with set new unwrapped line index - if (this._terminal._core.buffer.lines.get(y).isWrapped) { - cumulativeCols += this._terminal.cols; - } else { - cumulativeCols = 0; - findingRow = y; - } - - // Run _findInLine at unwrapped row, scan for cumulativeCols columns - result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); + result = this._findInLine(term, y, 0, searchOptions); if (result) { break; } @@ -94,23 +86,11 @@ export class SearchHelper implements ISearchHelper { // Search from the top to the startRow (search the whole startRow again in // case startCol > 0) if (!result) { - // Assume that The first line is always unwrapped line - let findingRow = 0; - // Scan at beginning of the line - let cumulativeCols = 0; - for (let y = 0; y <= startRow; y++) { - result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); + for (let y = 0; y < findingRow; y++) { + result = this._findInLine(term, y, 0, searchOptions); if (result) { break; } - // If the current line is wrapped line, increase index of beginning column - // So we ignore the previous scan - if (this._terminal._core.buffer.lines.get(y).isWrapped) { - cumulativeCols += this._terminal.cols; - } else { - cumulativeCols = 0; - findingRow = y; - } } } From e813ae02fc5a140c3c5d481f2c7ca184de887297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 1 Feb 2019 20:28:45 +0100 Subject: [PATCH 07/40] return true from inputhandler methods to signal sequence was handled --- src/InputHandler.ts | 183 +++++++++++++++++++++++++++++--------------- src/Types.ts | 108 +++++++++++++------------- 2 files changed, 176 insertions(+), 115 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 2f53cfcb..4af525f2 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -446,15 +446,16 @@ export class InputHandler extends Disposable implements IInputHandler { * BEL * Bell (Ctrl-G). */ - public bell(): void { + public bell(): boolean { this._terminal.bell(); + return true; } /** * LF * Line Feed or New Line (NL). (LF is Ctrl-J). */ - public lineFeed(): void { + public lineFeed(): boolean { // make buffer local for faster access const buffer = this._terminal.buffer; @@ -476,36 +477,40 @@ export class InputHandler extends Disposable implements IInputHandler { * @event linefeed */ this._terminal.emit('linefeed'); + return true; } /** * CR * Carriage Return (Ctrl-M). */ - public carriageReturn(): void { + public carriageReturn(): boolean { this._terminal.buffer.x = 0; + return true; } /** * BS * Backspace (Ctrl-H). */ - public backspace(): void { + public backspace(): boolean { if (this._terminal.buffer.x > 0) { this._terminal.buffer.x--; } + return true; } /** * TAB * Horizontal Tab (HT) (Ctrl-I). */ - public tab(): void { + public tab(): boolean { const originalX = this._terminal.buffer.x; this._terminal.buffer.x = this._terminal.buffer.nextStop(); if (this._terminal.options.screenReaderMode) { this._terminal.emit('a11y.tab', this._terminal.buffer.x - originalX); } + return true; } /** @@ -513,8 +518,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Shift Out (Ctrl-N) -> Switch to Alternate Character Set. This invokes the * G1 character set. */ - public shiftOut(): void { + public shiftOut(): boolean { this._terminal.setgLevel(1); + return true; } /** @@ -522,28 +528,30 @@ export class InputHandler extends Disposable implements IInputHandler { * Shift In (Ctrl-O) -> Switch to Standard Character Set. This invokes the G0 * character set (the default). */ - public shiftIn(): void { + public shiftIn(): boolean { this._terminal.setgLevel(0); + return true; } /** * CSI Ps @ * Insert Ps (Blank) Character(s) (default = 1) (ICH). */ - public insertChars(params: number[]): void { + public insertChars(params: number[]): boolean { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells( this._terminal.buffer.x, params[0] || 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] ); this._terminal.updateRange(this._terminal.buffer.y); + return true; } /** * CSI Ps A * Cursor Up Ps Times (default = 1) (CUU). */ - public cursorUp(params: number[]): void { + public cursorUp(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -552,13 +560,14 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.y < 0) { this._terminal.buffer.y = 0; } + return true; } /** * CSI Ps B * Cursor Down Ps Times (default = 1) (CUD). */ - public cursorDown(params: number[]): void { + public cursorDown(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -571,13 +580,14 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x--; } + return true; } /** * CSI Ps C * Cursor Forward Ps Times (default = 1) (CUF). */ - public cursorForward(params: number[]): void { + public cursorForward(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -586,13 +596,14 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } + return true; } /** * CSI Ps D * Cursor Backward Ps Times (default = 1) (CUB). */ - public cursorBackward(params: number[]): void { + public cursorBackward(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -605,6 +616,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x < 0) { this._terminal.buffer.x = 0; } + return true; } /** @@ -612,7 +624,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Next Line Ps Times (default = 1) (CNL). * same as CSI Ps B ? */ - public cursorNextLine(params: number[]): void { + public cursorNextLine(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -622,6 +634,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.y = this._terminal.rows - 1; } this._terminal.buffer.x = 0; + return true; } @@ -630,7 +643,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Preceding Line Ps Times (default = 1) (CNL). * reuse CSI Ps A ? */ - public cursorPrecedingLine(params: number[]): void { + public cursorPrecedingLine(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -640,6 +653,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.y = 0; } this._terminal.buffer.x = 0; + return true; } @@ -647,19 +661,20 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps G * Cursor Character Absolute [column] (default = [row,1]) (CHA). */ - public cursorCharAbsolute(params: number[]): void { + public cursorCharAbsolute(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; } this._terminal.buffer.x = param - 1; + return true; } /** * CSI Ps ; Ps H * Cursor Position [row;column] (default = [1,1]) (CUP). */ - public cursorPosition(params: number[]): void { + public cursorPosition(params: number[]): boolean { let col: number; let row: number = params[0] - 1; @@ -683,17 +698,19 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.x = col; this._terminal.buffer.y = row; + return true; } /** * CSI Ps I * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). */ - public cursorForwardTab(params: number[]): void { + public cursorForwardTab(params: number[]): boolean { let param = params[0] || 1; while (param--) { this._terminal.buffer.x = this._terminal.buffer.nextStop(); } + return true; } /** @@ -736,7 +753,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 -> Selective Erase Above. * Ps = 2 -> Selective Erase All. */ - public eraseInDisplay(params: number[]): void { + public eraseInDisplay(params: number[]): boolean { let j; switch (params[0]) { case 0: @@ -782,6 +799,7 @@ export class InputHandler extends Disposable implements IInputHandler { } break; } + return true; } /** @@ -795,7 +813,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 -> Selective Erase to Left. * Ps = 2 -> Selective Erase All. */ - public eraseInLine(params: number[]): void { + public eraseInLine(params: number[]): boolean { switch (params[0]) { case 0: this._eraseInBufferLine(this._terminal.buffer.y, this._terminal.buffer.x, this._terminal.cols); @@ -808,13 +826,14 @@ export class InputHandler extends Disposable implements IInputHandler { break; } this._terminal.updateRange(this._terminal.buffer.y); + return true; } /** * CSI Ps L * Insert Ps Line(s) (default = 1) (IL). */ - public insertLines(params: number[]): void { + public insertLines(params: number[]): boolean { let param: number = params[0]; if (param < 1) { param = 1; @@ -837,13 +856,14 @@ export class InputHandler extends Disposable implements IInputHandler { // this.maxRange(); this._terminal.updateRange(buffer.y); this._terminal.updateRange(buffer.scrollBottom); + return true; } /** * CSI Ps M * Delete Ps Line(s) (default = 1) (DL). */ - public deleteLines(params: number[]): void { + public deleteLines(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -867,25 +887,27 @@ export class InputHandler extends Disposable implements IInputHandler { // this.maxRange(); this._terminal.updateRange(buffer.y); this._terminal.updateRange(buffer.scrollBottom); + return true; } /** * CSI Ps P * Delete Ps Character(s) (default = 1) (DCH). */ - public deleteChars(params: number[]): void { + public deleteChars(params: number[]): boolean { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells( this._terminal.buffer.x, params[0] || 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] ); this._terminal.updateRange(this._terminal.buffer.y); + return true; } /** * CSI Ps S Scroll up Ps lines (default = 1) (SU). */ - public scrollUp(params: number[]): void { + public scrollUp(params: number[]): boolean { let param = params[0] || 1; // make buffer local for faster access @@ -898,12 +920,13 @@ export class InputHandler extends Disposable implements IInputHandler { // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); this._terminal.updateRange(buffer.scrollBottom); + return true; } /** * CSI Ps T Scroll down Ps lines (default = 1) (SD). */ - public scrollDown(params: number[], collect?: string): void { + public scrollDown(params: number[], collect?: string): boolean { if (params.length < 2 && !collect) { let param = params[0] || 1; @@ -918,24 +941,26 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.scrollTop); this._terminal.updateRange(buffer.scrollBottom); } + return true; } /** * CSI Ps X * Erase Ps Character(s) (default = 1) (ECH). */ - public eraseChars(params: number[]): void { + public eraseChars(params: number[]): boolean { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells( this._terminal.buffer.x, this._terminal.buffer.x + (params[0] || 1), [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] ); + return true; } /** * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). */ - public cursorBackwardTab(params: number[]): void { + public cursorBackwardTab(params: number[]): boolean { let param = params[0] || 1; // make buffer local for faster access @@ -944,13 +969,14 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.x = buffer.prevStop(); } + return true; } /** * CSI Pm ` Character Position Absolute * [column] (default = [row,1]) (HPA). */ - public charPosAbsolute(params: number[]): void { + public charPosAbsolute(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -959,6 +985,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } + return true; } /** @@ -966,7 +993,7 @@ export class InputHandler extends Disposable implements IInputHandler { * [columns] (default = [row,col+1]) (HPR) * reuse CSI Ps C ? */ - public hPositionRelative(params: number[]): void { + public hPositionRelative(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -975,12 +1002,13 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } + return true; } /** * CSI Ps b Repeat the preceding graphic character Ps times (REP). */ - public repeatPrecedingCharacter(params: number[]): void { + public repeatPrecedingCharacter(params: number[]): boolean { // make buffer local for faster access const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); @@ -989,6 +1017,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] ); // FIXME: no updateRange here? + return true; } /** @@ -1028,9 +1057,9 @@ export class InputHandler extends Disposable implements IInputHandler { * xterm/charproc.c - line 2012, for more information. * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) */ - public sendDeviceAttributes(params: number[], collect?: string): void { + public sendDeviceAttributes(params: number[], collect?: string): boolean { if (params[0] > 0) { - return; + return true; } if (!collect) { @@ -1055,13 +1084,14 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.handler(C0.ESC + '[>83;40003;0c'); } } + return true; } /** * CSI Pm d Vertical Position Absolute (VPA) * [row] (default = [1,column]) */ - public linePosAbsolute(params: number[]): void { + public linePosAbsolute(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -1070,6 +1100,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.y >= this._terminal.rows) { this._terminal.buffer.y = this._terminal.rows - 1; } + return true; } /** @@ -1077,7 +1108,7 @@ export class InputHandler extends Disposable implements IInputHandler { * [rows] (default = [row+1,column]) * reuse CSI Ps B ? */ - public vPositionRelative(params: number[]): void { + public vPositionRelative(params: number[]): boolean { let param = params[0]; if (param < 1) { param = 1; @@ -1090,6 +1121,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x--; } + return true; } /** @@ -1097,7 +1129,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Horizontal and Vertical Position [row;column] (default = * [1,1]) (HVP). */ - public hVPosition(params: number[]): void { + public hVPosition(params: number[]): boolean { if (params[0] < 1) params[0] = 1; if (params[1] < 1) params[1] = 1; @@ -1110,6 +1142,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } + return true; } /** @@ -1120,13 +1153,14 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 2 -> Clear Stops on Line. * http://vt100.net/annarbor/aaa-ug/section6.html */ - public tabClear(params: number[]): void { + public tabClear(params: number[]): boolean { const param = params[0]; if (param <= 0) { delete this._terminal.buffer.tabs[this._terminal.buffer.x]; } else if (param === 3) { this._terminal.buffer.tabs = {}; } + return true; } /** @@ -1215,13 +1249,13 @@ export class InputHandler extends Disposable implements IInputHandler { * Modes: * http: *vt100.net/docs/vt220-rm/chapter4.html */ - public setMode(params: number[], collect?: string): void { + public setMode(params: number[], collect?: string): boolean { if (params.length > 1) { for (let i = 0; i < params.length; i++) { this.setMode([params[i]]); } - return; + return true; } if (!collect) { @@ -1334,6 +1368,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } + return true; } /** @@ -1418,13 +1453,13 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. * Ps = 2 0 0 4 -> Reset bracketed paste mode. */ - public resetMode(params: number[], collect?: string): void { + public resetMode(params: number[], collect?: string): boolean { if (params.length > 1) { for (let i = 0; i < params.length; i++) { this.resetMode([params[i]]); } - return; + return true; } if (!collect) { @@ -1514,6 +1549,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } + return true; } /** @@ -1581,11 +1617,11 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 4 8 ; 5 ; Ps -> Set background color to the second * Ps. */ - public charAttributes(params: number[]): void { + public charAttributes(params: number[]): boolean { // Optimize a single SGR0. if (params.length === 1 && params[0] === 0) { this._terminal.curAttr = DEFAULT_ATTR; - return; + return true; } const l = params.length; @@ -1705,6 +1741,8 @@ export class InputHandler extends Disposable implements IInputHandler { } this._terminal.curAttr = (flags << 18) | (fg << 9) | bg; + + return true; } /** @@ -1730,7 +1768,7 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI ? 5 3 n Locator available, if compiled-in, or * CSI ? 5 0 n No Locator, if not. */ - public deviceStatus(params: number[], collect?: string): void { + public deviceStatus(params: number[], collect?: string): boolean { if (!collect) { switch (params[0]) { case 5: @@ -1772,13 +1810,14 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } + return true; } /** * CSI ! p Soft terminal reset (DECSTR). * http://vt100.net/docs/vt220-rm/table4-10.html */ - public softReset(params: number[], collect?: string): void { + public softReset(params: number[], collect?: string): boolean { if (collect === '!') { this._terminal.cursorHidden = false; this._terminal.insertMode = false; @@ -1797,6 +1836,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.glevel = 0; // ?? this._terminal.charsets = [null]; // ?? } + return true; } /** @@ -1809,7 +1849,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 5 -> blinking bar (xterm). * Ps = 6 -> steady bar (xterm). */ - public setCursorStyle(params?: number[], collect?: string): void { + public setCursorStyle(params?: number[], collect?: string): boolean { if (collect === ' ') { const param = params[0] < 1 ? 1 : params[0]; switch (param) { @@ -1829,6 +1869,7 @@ export class InputHandler extends Disposable implements IInputHandler { const isBlinking = param % 2 === 1; this._terminal.setOption('cursorBlink', isBlinking); } + return true; } /** @@ -1837,12 +1878,15 @@ export class InputHandler extends Disposable implements IInputHandler { * dow) (DECSTBM). * CSI ? Pm r */ - public setScrollRegion(params: number[], collect?: string): void { - if (collect) return; + public setScrollRegion(params: number[], collect?: string): boolean { + if (collect) { + return true; + } this._terminal.buffer.scrollTop = (params[0] || 1) - 1; this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1; this._terminal.buffer.x = 0; this._terminal.buffer.y = 0; + return true; } @@ -1851,10 +1895,11 @@ export class InputHandler extends Disposable implements IInputHandler { * ESC 7 * Save cursor (ANSI.SYS). */ - public saveCursor(params: number[]): void { + public saveCursor(params: number[]): boolean { this._terminal.buffer.savedX = this._terminal.buffer.x; this._terminal.buffer.savedY = this._terminal.buffer.y; this._terminal.buffer.savedCurAttr = this._terminal.curAttr; + return true; } @@ -1863,10 +1908,11 @@ export class InputHandler extends Disposable implements IInputHandler { * ESC 8 * Restore cursor (ANSI.SYS). */ - public restoreCursor(params: number[]): void { + public restoreCursor(params: number[]): boolean { this._terminal.buffer.x = this._terminal.buffer.savedX || 0; this._terminal.buffer.y = this._terminal.buffer.savedY || 0; this._terminal.curAttr = this._terminal.buffer.savedCurAttr || DEFAULT_ATTR; + return true; } @@ -1875,8 +1921,9 @@ export class InputHandler extends Disposable implements IInputHandler { * OSC 2; ST (set window title) * Proxy to set window title. Icon name is not supported. */ - public setTitle(data: string): void { + public setTitle(data: string): boolean { this._terminal.handleTitle(data); + return true; } /** @@ -1885,9 +1932,10 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL) * Moves cursor to first position on next line. */ - public nextLine(): void { + public nextLine(): boolean { this._terminal.buffer.x = 0; this.index(); + return true; } /** @@ -1895,12 +1943,13 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html) * Enables the numeric keypad to send application sequences to the host. */ - public keypadApplicationMode(): void { + public keypadApplicationMode(): boolean { this._terminal.log('Serial port requested application keypad.'); this._terminal.applicationKeypad = true; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } + return true; } /** @@ -1908,12 +1957,13 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html) * Enables the keypad to send numeric characters to the host. */ - public keypadNumericMode(): void { + public keypadNumericMode(): boolean { this._terminal.log('Switching back to normal keypad.'); this._terminal.applicationKeypad = false; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } + return true; } /** @@ -1922,9 +1972,10 @@ export class InputHandler extends Disposable implements IInputHandler { * Select default character set. UTF-8 is not supported (string are unicode anyways) * therefore ESC % G does the same. */ - public selectDefaultCharset(): void { + public selectDefaultCharset(): boolean { this._terminal.setgLevel(0); this._terminal.setgCharset(0, DEFAULT_CHARSET); // US (default) + return true; } /** @@ -1943,10 +1994,15 @@ export class InputHandler extends Disposable implements IInputHandler { * ESC / C * Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported? */ - public selectCharset(collectAndFlag: string): void { - if (collectAndFlag.length !== 2) return this.selectDefaultCharset(); - if (collectAndFlag[0] === '/') return; // TODO: Is this supported? + public selectCharset(collectAndFlag: string): boolean { + if (collectAndFlag.length !== 2) { + return this.selectDefaultCharset(); + } + if (collectAndFlag[0] === '/') { + return true; // TODO: Is this supported? + } this._terminal.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); + return true; } /** @@ -1955,8 +2011,9 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html) * Moves the cursor down one line in the same column. */ - public index(): void { + public index(): boolean { this._terminal.index(); // TODO: save to move from terminal? + return true; } /** @@ -1966,8 +2023,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Sets a horizontal tab stop at the column position indicated by * the value of the active column when the terminal receives an HTS. */ - public tabSet(): void { + public tabSet(): boolean { this._terminal.tabSet(); // TODO: save to move from terminal? + return true; } /** @@ -1977,8 +2035,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Moves the cursor up one line in the same column. If the cursor is at the top margin, * the page scrolls down. */ - public reverseIndex(): void { + public reverseIndex(): boolean { this._terminal.reverseIndex(); // TODO: save to move from terminal? + return true; } /** @@ -1986,9 +2045,10 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html) * Reset to initial state. */ - public reset(): void { + public reset(): boolean { this._parser.reset(); this._terminal.reset(); // TODO: save to move from terminal? + return true; } /** @@ -2001,7 +2061,8 @@ export class InputHandler extends Disposable implements IInputHandler { * When you use a locking shift, the character set remains in GL or GR until * you use another locking shift. (partly supported) */ - public setgLevel(level: number): void { + public setgLevel(level: number): boolean { this._terminal.setgLevel(level); // TODO: save to move from terminal? + return true; } } diff --git a/src/Types.ts b/src/Types.ts index cc8ff00a..1611fb55 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -113,73 +113,73 @@ export interface IInputHandler { parse(data: string): void; print(data: Uint32Array, start: number, end: number): void; - /** C0 BEL */ bell(): void; - /** C0 LF */ lineFeed(): void; - /** C0 CR */ carriageReturn(): void; - /** C0 BS */ backspace(): void; - /** C0 HT */ tab(): void; - /** C0 SO */ shiftOut(): void; - /** C0 SI */ shiftIn(): void; + /** C0 BEL */ bell(): boolean; + /** C0 LF */ lineFeed(): boolean; + /** C0 CR */ carriageReturn(): boolean; + /** C0 BS */ backspace(): boolean; + /** C0 HT */ tab(): boolean; + /** C0 SO */ shiftOut(): boolean; + /** C0 SI */ shiftIn(): boolean; - /** CSI @ */ insertChars(params?: number[]): void; - /** CSI A */ cursorUp(params?: number[]): void; - /** CSI B */ cursorDown(params?: number[]): void; - /** CSI C */ cursorForward(params?: number[]): void; - /** CSI D */ cursorBackward(params?: number[]): void; - /** CSI E */ cursorNextLine(params?: number[]): void; - /** CSI F */ cursorPrecedingLine(params?: number[]): void; - /** CSI G */ cursorCharAbsolute(params?: number[]): void; - /** CSI H */ cursorPosition(params?: number[]): void; - /** CSI I */ cursorForwardTab(params?: number[]): void; - /** CSI J */ eraseInDisplay(params?: number[]): void; - /** CSI K */ eraseInLine(params?: number[]): void; - /** CSI L */ insertLines(params?: number[]): void; - /** CSI M */ deleteLines(params?: number[]): void; - /** CSI P */ deleteChars(params?: number[]): void; - /** CSI S */ scrollUp(params?: number[]): void; - /** CSI T */ scrollDown(params?: number[], collect?: string): void; - /** CSI X */ eraseChars(params?: number[]): void; - /** CSI Z */ cursorBackwardTab(params?: number[]): void; - /** CSI ` */ charPosAbsolute(params?: number[]): void; - /** CSI a */ hPositionRelative(params?: number[]): void; - /** CSI b */ repeatPrecedingCharacter(params?: number[]): void; - /** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): void; - /** CSI d */ linePosAbsolute(params?: number[]): void; - /** CSI e */ vPositionRelative(params?: number[]): void; - /** CSI f */ hVPosition(params?: number[]): void; - /** CSI g */ tabClear(params?: number[]): void; - /** CSI h */ setMode(params?: number[], collect?: string): void; - /** CSI l */ resetMode(params?: number[], collect?: string): void; - /** CSI m */ charAttributes(params?: number[]): void; - /** CSI n */ deviceStatus(params?: number[], collect?: string): void; - /** CSI p */ softReset(params?: number[], collect?: string): void; - /** CSI q */ setCursorStyle(params?: number[], collect?: string): void; - /** CSI r */ setScrollRegion(params?: number[], collect?: string): void; - /** CSI s */ saveCursor(params?: number[]): void; - /** CSI u */ restoreCursor(params?: number[]): void; + /** CSI @ */ insertChars(params?: number[]): boolean; + /** CSI A */ cursorUp(params?: number[]): boolean; + /** CSI B */ cursorDown(params?: number[]): boolean; + /** CSI C */ cursorForward(params?: number[]): boolean; + /** CSI D */ cursorBackward(params?: number[]): boolean; + /** CSI E */ cursorNextLine(params?: number[]): boolean; + /** CSI F */ cursorPrecedingLine(params?: number[]): boolean; + /** CSI G */ cursorCharAbsolute(params?: number[]): boolean; + /** CSI H */ cursorPosition(params?: number[]): boolean; + /** CSI I */ cursorForwardTab(params?: number[]): boolean; + /** CSI J */ eraseInDisplay(params?: number[]): boolean; + /** CSI K */ eraseInLine(params?: number[]): boolean; + /** CSI L */ insertLines(params?: number[]): boolean; + /** CSI M */ deleteLines(params?: number[]): boolean; + /** CSI P */ deleteChars(params?: number[]): boolean; + /** CSI S */ scrollUp(params?: number[]): boolean; + /** CSI T */ scrollDown(params?: number[], collect?: string): boolean; + /** CSI X */ eraseChars(params?: number[]): boolean; + /** CSI Z */ cursorBackwardTab(params?: number[]): boolean; + /** CSI ` */ charPosAbsolute(params?: number[]): boolean; + /** CSI a */ hPositionRelative(params?: number[]): boolean; + /** CSI b */ repeatPrecedingCharacter(params?: number[]): boolean; + /** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): boolean; + /** CSI d */ linePosAbsolute(params?: number[]): boolean; + /** CSI e */ vPositionRelative(params?: number[]): boolean; + /** CSI f */ hVPosition(params?: number[]): boolean; + /** CSI g */ tabClear(params?: number[]): boolean; + /** CSI h */ setMode(params?: number[], collect?: string): boolean; + /** CSI l */ resetMode(params?: number[], collect?: string): boolean; + /** CSI m */ charAttributes(params?: number[]): boolean; + /** CSI n */ deviceStatus(params?: number[], collect?: string): boolean; + /** CSI p */ softReset(params?: number[], collect?: string): boolean; + /** CSI q */ setCursorStyle(params?: number[], collect?: string): boolean; + /** CSI r */ setScrollRegion(params?: number[], collect?: string): boolean; + /** CSI s */ saveCursor(params?: number[]): boolean; + /** CSI u */ restoreCursor(params?: number[]): boolean; /** OSC 0 - OSC 2 */ setTitle(data: string): void; - /** ESC E */ nextLine(): void; - /** ESC = */ keypadApplicationMode(): void; - /** ESC > */ keypadNumericMode(): void; + OSC 2 */ setTitle(data: string): boolean; + /** ESC E */ nextLine(): boolean; + /** ESC = */ keypadApplicationMode(): boolean; + /** ESC > */ keypadNumericMode(): boolean; /** ESC % G - ESC % @ */ selectDefaultCharset(): void; + ESC % @ */ selectDefaultCharset(): boolean; /** ESC ( C ESC ) C ESC * C ESC + C ESC - C ESC . C - ESC / C */ selectCharset(collectAndFlag: string): void; - /** ESC D */ index(): void; - /** ESC H */ tabSet(): void; - /** ESC M */ reverseIndex(): void; - /** ESC c */ reset(): void; + ESC / C */ selectCharset(collectAndFlag: string): boolean; + /** ESC D */ index(): boolean; + /** ESC H */ tabSet(): boolean; + /** ESC M */ reverseIndex(): boolean; + /** ESC c */ reset(): boolean; /** ESC n ESC o ESC | ESC } - ESC ~ */ setgLevel(level: number): void; + ESC ~ */ setgLevel(level: number): boolean; } export interface ILinkMatcher { From 801db3831ddb7980955c2af759913aed3120f64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 1 Feb 2019 20:32:07 +0100 Subject: [PATCH 08/40] type add methods --- src/InputHandler.ts | 11 +++++++++-- src/Types.ts | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 4af525f2..05af14a2 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -435,10 +435,17 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.y); } - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + /** + * Forward addCsiHandler from parser. + */ + public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { return this._parser.addCsiHandler(flag, callback); } - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + + /** + * Forward addOscHandler from parser. + */ + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { return this._parser.addOscHandler(ident, callback); } diff --git a/src/Types.ts b/src/Types.ts index 1611fb55..67ad32d9 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -112,6 +112,8 @@ export interface ICompositionHelper { export interface IInputHandler { parse(data: string): void; print(data: Uint32Array, start: number, end: number): void; + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; /** C0 BEL */ bell(): boolean; /** C0 LF */ lineFeed(): boolean; From 03e648a0fbc250438d28001036261bba951292ef Mon Sep 17 00:00:00 2001 From: ntchjb Date: Fri, 15 Mar 2019 19:15:41 +0700 Subject: [PATCH 09/40] Fixed the case that, if findPrevious is run without any selection, then start scan at the last row --- src/addons/search/SearchHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 525e25dc..a2258e48 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -115,7 +115,7 @@ export class SearchHelper implements ISearchHelper { } const isReverseSearch = true; - let startRow = this._terminal._core.buffer.ydisp; + let startRow = this._terminal.rows-1; let startCol: number = this._terminal.cols; if (selectionManager.selectionStart) { From 3c444ca39a6c52eaacc1e7fedb3ab08a670a89d5 Mon Sep 17 00:00:00 2001 From: ntchjb Date: Fri, 15 Mar 2019 19:20:23 +0700 Subject: [PATCH 10/40] Fixed coding format --- src/addons/search/SearchHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index a2258e48..57a8c0a8 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -115,7 +115,7 @@ export class SearchHelper implements ISearchHelper { } const isReverseSearch = true; - let startRow = this._terminal.rows-1; + let startRow = this._terminal.rows - 1; let startCol: number = this._terminal.cols; if (selectionManager.selectionStart) { From ca1f7f1392203c9f601ad3f68dd818fb1bde025a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 10:11:59 -0700 Subject: [PATCH 11/40] Add EventEmitter2 --- src/common/EventEmitter2.test.ts | 31 +++++++++++++++++++++ src/common/EventEmitter2.ts | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/common/EventEmitter2.test.ts create mode 100644 src/common/EventEmitter2.ts diff --git a/src/common/EventEmitter2.test.ts b/src/common/EventEmitter2.test.ts new file mode 100644 index 00000000..53ee4c4f --- /dev/null +++ b/src/common/EventEmitter2.test.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { EventEmitter2 } from './EventEmitter2'; + +describe('EventEmitter2', () => { + it('should fire listeners multiple times', () => { + const order: string[] = []; + const emitter = new EventEmitter2(); + emitter.event(data => order.push(data + 'a')); + emitter.event(data => order.push(data + 'b')); + emitter.fire(1); + emitter.fire(2); + assert.deepEqual(order, [ '1a', '1b', '2a', '2b' ]); + }); + + it('should not fire listeners once disposed', () => { + const order: string[] = []; + const emitter = new EventEmitter2(); + emitter.event(data => order.push(data + 'a')); + const disposeB = emitter.event(data => order.push(data + 'b')); + emitter.event(data => order.push(data + 'c')); + emitter.fire(1); + disposeB.dispose(); + emitter.fire(2); + assert.deepEqual(order, [ '1a', '1b', '1c', '2a', '2c' ]); + }); +}); diff --git a/src/common/EventEmitter2.ts b/src/common/EventEmitter2.ts new file mode 100644 index 00000000..447f816c --- /dev/null +++ b/src/common/EventEmitter2.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'xterm'; + +type Listener = (e: T) => void; + +export interface IEvent { + (listener: (e: T) => any): IDisposable; +} + +export class EventEmitter2 { + private _listeners: Listener[] = []; + private _event?: IEvent; + + public get event(): IEvent { + if (!this._event) { + this._event = (listener: (e: T) => any) => { + this._listeners.push(listener); + const disposable = { + dispose: () => { + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] === listener) { + this._listeners.splice(i, 1); + return; + } + } + } + }; + return disposable; + }; + } + return this._event; + } + + public fire(data: T): void { + const queue: Listener[] = []; + for (let i = 0; i < this._listeners.length; i++) { + queue.push(this._listeners[i]); + } + for (let i = 0; i < queue.length; i++) { + queue[i].call(undefined, data); + } + } +} From ba4662ac147968bf44494a5db0d22d66913dd762 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 11:01:16 -0700 Subject: [PATCH 12/40] Introduce new onEvent APIs and deprecate on/off/etc. Part of #1505 --- src/Terminal.ts | 28 +++++++++++++++ src/public/Terminal.ts | 10 ++++++ typings/xterm.d.ts | 77 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/src/Terminal.ts b/src/Terminal.ts index db696bd6..8c69a551 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -52,6 +52,7 @@ import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { KeyboardResultType, ICharset } from './core/Types'; import { clone } from './common/Clone'; +import { EventEmitter2, IEvent } from './common/EventEmitter2'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -217,6 +218,23 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public cols: number; public rows: number; + private _onLineFeed = new EventEmitter2(); + public get onLineFeed(): IEvent { return this._onLineFeed.event; } + private _onSelectionChange = new EventEmitter2(); + public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } + private _onInput = new EventEmitter2(); + public get onInput(): IEvent { return this._onInput.event; } + private _onTitleChange = new EventEmitter2(); + public get onTitleChange(): IEvent { return this._onTitleChange.event; } + private _onScroll = new EventEmitter2(); + public get onScroll(): IEvent { return this._onScroll.event; } + private _onKey = new EventEmitter2<{ key: string, domEvent: KeyboardEvent }>(); + public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } + private _onRender = new EventEmitter2<{ start: number, end: number }>(); + public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } + private _onResize = new EventEmitter2<{ cols: number, rows: number }>(); + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } + /** * Creates a new `Terminal` object. * @@ -235,6 +253,16 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II super(); this.options = clone(options); this._setup(); + + // TODO: Replace EventEmitter with EventEmitter2 internally + this.on('linefeed', () => this._onLineFeed.fire()); + this.on('selection', () => this._onSelectionChange.fire()); + this.on('data', e => this._onInput.fire(e)); + this.on('title', e => this._onTitleChange.fire(e)); + this.on('scroll', e => this._onScroll.fire(e)); + this.on('key', e => this._onKey.fire(e)); + this.on('refresh', e => this._onRender.fire(e)); + this.on('resize', e => this._onResize.fire(e)); } public dispose(): void { diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 87fcfaef..181beeeb 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -7,6 +7,7 @@ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILink import { ITerminal } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; +import { IEvent } from '../../lib/common/EventEmitter2'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -15,6 +16,15 @@ export class Terminal implements ITerminalApi { this._core = new TerminalCore(options); } + public get onLineFeed(): IEvent { return this._core.onLineFeed; } + public get onSelectionChange(): IEvent { return this._core.onSelectionChange; } + public get onInput(): IEvent { return this._core.onInput; } + public get onTitleChange(): IEvent { return this._core.onTitleChange; } + public get onScroll(): IEvent { return this._core.onScroll; } + public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; } + public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; } + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } + public get element(): HTMLElement { return this._core.element; } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d813bc5f..0a29cfc8 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -295,6 +295,14 @@ declare module 'xterm' { dispose(): void; } + /** + * An event that can be listened to. + * @returns an `IDisposable` to stop listening. + */ + export interface IEvent { + (listener: (e: T) => any): IDisposable; + } + export interface IMarker extends IDisposable { readonly id: number; readonly isDisposed: boolean; @@ -353,6 +361,64 @@ declare module 'xterm' { */ constructor(options?: ITerminalOptions); + /** + * Adds an event listener for when a line feed is added. + * @returns an `IDisposable` to stop listening. + */ + onLineFeed: IEvent; + + /** + * Adds an event listener for when a selection change occurs. + * @returns an `IDisposable` to stop listening. + */ + onSelectionChange: IEvent; + + /** + * Adds an event listener for when an input event fires. This happens for + * example when the user types or pastes into the terminal. The event value + * is whatever `string` results, in a typical setup, this should be passed + * on to the backing pty. + * @returns an `IDisposable` to stop listening. + */ + onInput: IEvent; + + /** + * Adds an event listener for when an OSC 0 or OSC 2 title change occurs. + * The event value is the new title. + * @returns an `IDisposable` to stop listening. + */ + onTitleChange: IEvent; + + /** + * Adds an event listener for when a scroll occurs. The event value is the + * new position of the viewport. + * @returns an `IDisposable` to stop listening. + */ + onScroll: IEvent; + + /** + * Adds an event listener for a key is pressed. The event value contains the + * string that will be sent in the data event as well as the DOM event that + * triggered it. + * @returns an `IDisposable` to stop listening. + */ + onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; + + /** + * Adds an event listener for when rows are rendered. The event value + * contains the start row and end rows of the rendered area (ranges from `0` + * to `Terminal.rows - 1`). + * @returns an `IDisposable` to stop listening. + */ + onRender: IEvent<{ start: number, end: number }>; + + /** + * Adds an event listener for when the terminal is resized. The event value + * contains the new size. + * @returns an `IDisposable` to stop listening. + */ + onResize: IEvent<{ cols: number, rows: number }>; + /** * Unfocus the terminal. */ @@ -367,54 +433,63 @@ declare module 'xterm' { * Registers an event listener. * @param type The type of the event. * @param listener The listener. + * @deprecated use `Terminal.onEvent(listener)` instead. */ on(type: 'blur' | 'focus' | 'linefeed' | 'selection', listener: () => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. + * @deprecated use `Terminal.onEvent(listener)` instead. */ on(type: 'data', listener: (...args: any[]) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. + * @deprecated use `Terminal.onEvent(listener)` instead. */ on(type: 'key', listener: (key: string, event: KeyboardEvent) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. + * @deprecated use `Terminal.onEvent(listener)` instead. */ on(type: 'keypress' | 'keydown', listener: (event: KeyboardEvent) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. + * @deprecated use `Terminal.onEvent(listener)` instead. */ on(type: 'refresh', listener: (data: {start: number, end: number}) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. + * @deprecated use `Terminal.onEvent(listener)` instead. */ on(type: 'resize', listener: (data: {cols: number, rows: number}) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. + * @deprecated use `Terminal.onEvent(listener)` instead. */ on(type: 'scroll', listener: (ydisp: number) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. + * @deprecated use `Terminal.onEvent(listener)` instead. */ on(type: 'title', listener: (title: string) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. + * @deprecated use `Terminal.onEvent(listener)` instead. */ on(type: string, listener: (...args: any[]) => void): void; @@ -422,6 +497,7 @@ declare module 'xterm' { * Deregisters an event listener. * @param type The type of the event. * @param listener The listener. + * @deprecated use `Terminal.onEvent(listener).dispose()` instead. */ off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void; @@ -439,6 +515,7 @@ declare module 'xterm' { * be used to conveniently remove the event listener. * @param type The type of event. * @param handler The event handler. + * @deprecated use `Terminal.onEvent(listener)` instead. */ addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; From 5f014be057db713055a0038379abbb4f2984d347 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 11:12:57 -0700 Subject: [PATCH 13/40] Add onCursorMove, replace .on usage in addons --- src/Terminal.ts | 3 +++ src/addons/search/SearchHelper.ts | 9 +++++++-- src/addons/terminado/terminado.ts | 4 ++-- src/addons/winptyCompat/winptyCompat.ts | 2 +- src/public/Terminal.ts | 1 + src/ui/TestUtils.test.ts | 11 ++++++++++- typings/xterm.d.ts | 6 ++++++ 7 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 8c69a551..1f05ec2e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -218,6 +218,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public cols: number; public rows: number; + private _onCursorMove = new EventEmitter2(); + public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onLineFeed = new EventEmitter2(); public get onLineFeed(): IEvent { return this._onLineFeed.event; } private _onSelectionChange = new EventEmitter2(); @@ -255,6 +257,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._setup(); // TODO: Replace EventEmitter with EventEmitter2 internally + this.on('cursormove', () => this._onCursorMove.fire()); this.on('linefeed', () => this._onLineFeed.fire()); this.on('selection', () => this._onSelectionChange.fire()); this.on('data', e => this._onInput.fire(e)); diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 96cd845d..7562efdf 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -4,6 +4,7 @@ */ import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces'; +import { IDisposable } from 'xterm'; const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs @@ -19,6 +20,7 @@ export class SearchHelper implements ISearchHelper { */ private _linesCache: string[] = null; private _linesCacheTimeoutId = 0; + private _cursorMoveListener: IDisposable | undefined; constructor(private _terminal: ISearchAddonTerminal) { this._destroyLinesCache = this._destroyLinesCache.bind(this); @@ -148,7 +150,7 @@ export class SearchHelper implements ISearchHelper { private _initLinesCache(): void { if (!this._linesCache) { this._linesCache = new Array(this._terminal._core.buffer.length); - this._terminal.on('cursormove', this._destroyLinesCache); + this._cursorMoveListener = this._terminal.onCursorMove(() => this._destroyLinesCache()); } window.clearTimeout(this._linesCacheTimeoutId); @@ -157,7 +159,10 @@ export class SearchHelper implements ISearchHelper { private _destroyLinesCache(): void { this._linesCache = null; - this._terminal.off('cursormove', this._destroyLinesCache); + if (this._cursorMoveListener) { + this._cursorMoveListener.dispose(); + this._cursorMoveListener = undefined; + } if (this._linesCacheTimeoutId) { window.clearTimeout(this._linesCacheTimeoutId); this._linesCacheTimeoutId = 0; diff --git a/src/addons/terminado/terminado.ts b/src/addons/terminado/terminado.ts index 136eea8e..b36c2cbf 100644 --- a/src/addons/terminado/terminado.ts +++ b/src/addons/terminado/terminado.ts @@ -59,9 +59,9 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional socket.addEventListener('message', addonTerminal.__getMessage); if (bidirectional) { - addonTerminal.on('data', addonTerminal.__sendData); + addonTerminal.onInput(addonTerminal.__sendData); } - addonTerminal.on('resize', addonTerminal.__setSize); + addonTerminal.onResize(addonTerminal.__setSize); socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket)); socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket)); diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 58f59fd9..59044613 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -25,7 +25,7 @@ export function winptyCompatInit(terminal: Terminal): void { // space. This is certainly not without its problems, but generally on // Windows when text reaches the end of the terminal it's likely going to be // wrapped. - addonTerminal.on('linefeed', () => { + addonTerminal.onLineFeed(() => { const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1); const lastChar = line.get(addonTerminal.cols - 1); diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 181beeeb..60b275f3 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -16,6 +16,7 @@ export class Terminal implements ITerminalApi { this._core = new TerminalCore(options); } + public get onCursorMove(): IEvent { return this._core.onCursorMove; } public get onLineFeed(): IEvent { return this._core.onLineFeed; } public get onSelectionChange(): IEvent { return this._core.onSelectionChange; } public get onInput(): IEvent { return this._core.onInput; } diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 9d525fbf..2e23fe70 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -8,7 +8,7 @@ import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuff import { ICircularList, XtermListener } from '../common/Types'; import { Buffer } from '../Buffer'; import * as Browser from '../core/Platform'; -import { ITheme, IDisposable, IMarker } from 'xterm'; +import { ITheme, IDisposable, IMarker, IEvent } from 'xterm'; import { Terminal } from '../Terminal'; export class TestTerminal extends Terminal { @@ -19,6 +19,15 @@ export class TestTerminal extends Terminal { } export class MockTerminal implements ITerminal { + onCursorMove: IEvent; + onLineFeed: IEvent; + onSelectionChange: IEvent; + onInput: IEvent; + onTitleChange: IEvent; + onScroll: IEvent; + onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; + onRender: IEvent<{ start: number; end: number; }>; + onResize: IEvent<{ cols: number; rows: number; }>; markers: IMarker[]; addMarker(cursorYOffset: number): IMarker { throw new Error('Method not implemented.'); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 0a29cfc8..e51c1167 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -361,6 +361,12 @@ declare module 'xterm' { */ constructor(options?: ITerminalOptions); + /** + * Adds an event listener for the cursor moves. + * @returns an `IDisposable` to stop listening. + */ + onCursorMove: IEvent; + /** * Adds an event listener for when a line feed is added. * @returns an `IDisposable` to stop listening. From f8fb4da9f49585389f53432ed1b0eaaf66f1fa10 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 11:30:47 -0700 Subject: [PATCH 14/40] Replace EventEmitter with EventEmitter2 in CircularList --- src/Buffer.test.ts | 4 ++-- src/Buffer.ts | 14 +++++++------- src/BufferReflow.ts | 6 +++--- src/SelectionManager.ts | 14 ++++++++------ src/common/CircularList.ts | 22 ++++++++++++++-------- src/common/Types.ts | 11 ++++++++++- 6 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 59475adb..72c07dc7 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -1025,7 +1025,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); const marker = buffer.addMarker(buffer.lines.length - 1); assert.equal(marker.line, buffer.lines.length - 1); - buffer.lines.emit('trim', 1); + buffer.lines.onTrimEmitter.fire(1); assert.equal(marker.line, buffer.lines.length - 2); }); it('should dispose of a marker if it is trimmed off the buffer', () => { @@ -1036,7 +1036,7 @@ describe('Buffer', () => { const marker = buffer.addMarker(0); assert.equal(marker.isDisposed, false); assert.equal(buffer.markers.length, 1); - buffer.lines.emit('trim', 1); + buffer.lines.onTrimEmitter.fire(1); assert.equal(marker.isDisposed, true); assert.equal(buffer.markers.length, 0); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index 9cc1adba..790667b6 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CircularList, IInsertEvent, IDeleteEvent } from './common/CircularList'; +import { CircularList, IInsertEvent } from './common/CircularList'; import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; @@ -442,7 +442,7 @@ export class Buffer implements IBuffer { insertEvents.push({ index: originalLineIndex + 1, amount: nextToInsert.newLines.length - } as IInsertEvent); + }); countInsertedSoFar += nextToInsert.newLines.length; nextToInsert = toInsert[++nextToInsertIndex]; @@ -455,12 +455,12 @@ export class Buffer implements IBuffer { let insertCountEmitted = 0; for (let i = insertEvents.length - 1; i >= 0; i--) { insertEvents[i].index += insertCountEmitted; - this.lines.emit('insert', insertEvents[i]); + this.lines.onInsertEmitter.fire(insertEvents[i]); insertCountEmitted += insertEvents[i].amount; } const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength); if (amountToTrim > 0) { - this.lines.emitMayRemoveListeners('trim', amountToTrim); + this.lines.onTrimEmitter.fire(amountToTrim); } } } @@ -580,19 +580,19 @@ export class Buffer implements IBuffer { public addMarker(y: number): Marker { const marker = new Marker(y); this.markers.push(marker); - marker.register(this.lines.addDisposableListener('trim', amount => { + marker.register(this.lines.onTrim(amount => { marker.line -= amount; // The marker should be disposed when the line is trimmed from the buffer if (marker.line < 0) { marker.dispose(); } })); - marker.register(this.lines.addDisposableListener('insert', (event: IInsertEvent) => { + marker.register(this.lines.onInsert(event => { if (marker.line >= event.index) { marker.line += event.amount; } })); - marker.register(this.lines.addDisposableListener('delete', (event: IDeleteEvent) => { + marker.register(this.lines.onDelete(event => { // Delete the marker if it's within the range if (marker.line >= event.index && marker.line < event.index + event.amount) { marker.dispose(); diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index d27d7c48..afe336d0 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -4,7 +4,7 @@ */ import { BufferLine, CellData } from './BufferLine'; -import { CircularList, IDeleteEvent } from './common/CircularList'; +import { CircularList } from './common/CircularList'; import { IBufferLine } from './Types'; import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; @@ -120,10 +120,10 @@ export function reflowLargerCreateNewLayout(lines: CircularList, to const countToRemove = toRemove[++nextToRemoveIndex]; // Tell markers that there was a deletion - lines.emit('delete', { + lines.onDeleteEmitter.fire({ index: i - countRemovedSoFar, amount: countToRemove - } as IDeleteEvent); + }); i += countToRemove - 1; countRemovedSoFar += countToRemove; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 361b1123..702f6790 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -4,7 +4,6 @@ */ import { ITerminal, ISelectionManager, IBuffer, IBufferLine } from './Types'; -import { XtermListener } from './common/Types'; import { MouseHelper } from './ui/MouseHelper'; import * as Browser from './core/Platform'; import { CharMeasure } from './ui/CharMeasure'; @@ -12,6 +11,7 @@ import { EventEmitter } from './common/EventEmitter'; import { SelectionModel } from './SelectionModel'; import { AltClickHandler } from './handlers/AltClickHandler'; import { CellData } from './BufferLine'; +import { IDisposable } from 'xterm'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -102,7 +102,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _mouseMoveListener: EventListener; private _mouseUpListener: EventListener; - private _trimListener: XtermListener; + private _trimListener: IDisposable; private _workCell: CellData = new CellData(); private _mouseDownTimeStamp: number; @@ -134,13 +134,12 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _initListeners(): void { this._mouseMoveListener = event => this._onMouseMove(event); this._mouseUpListener = event => this._onMouseUp(event); - this._trimListener = (amount: number) => this._onTrim(amount); this.initBuffersListeners(); } public initBuffersListeners(): void { - this._terminal.buffer.lines.on('trim', this._trimListener); + this._trimListener = this._terminal.buffer.lines.onTrim(amount => this._onTrim(amount)); this._terminal.buffers.on('activate', e => this._onBufferActivate(e)); } @@ -337,6 +336,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param amount The amount the buffer is being trimmed. */ private _onTrim(amount: number): void { + console.log('onTrim', amount); const needsRefresh = this._model.onTrim(amount); if (needsRefresh) { this.refresh(); @@ -657,8 +657,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // reverseIndex) and delete in a splice is only ever used when the same // number of elements was just added. Given this is could actually be // beneficial to leave the selection as is for these cases. - e.inactiveBuffer.lines.off('trim', this._trimListener); - e.activeBuffer.lines.on('trim', this._trimListener); + if (this._trimListener) { + this._trimListener.dispose(); + } + this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount)); } /** diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 90891b72..d4fc41cd 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { EventEmitter } from './EventEmitter'; import { ICircularList } from './Types'; +import { EventEmitter2, IEvent } from './EventEmitter2'; export interface IInsertEvent { index: number; @@ -20,15 +20,21 @@ export interface IDeleteEvent { * Represents a circular list; a list with a maximum size that wraps around when push is called, * overriding values at the start of the list. */ -export class CircularList extends EventEmitter implements ICircularList { +export class CircularList implements ICircularList { protected _array: (T | undefined)[]; private _startIndex: number; private _length: number; + public onDeleteEmitter = new EventEmitter2(); + public get onDelete(): IEvent { return this.onDeleteEmitter.event; } + public onInsertEmitter = new EventEmitter2(); + public get onInsert(): IEvent { return this.onInsertEmitter.event; } + public onTrimEmitter = new EventEmitter2(); + public get onTrim(): IEvent { return this.onTrimEmitter.event; } + constructor( private _maxLength: number ) { - super(); this._array = new Array(this._maxLength); this._startIndex = 0; this._length = 0; @@ -101,7 +107,7 @@ export class CircularList extends EventEmitter implements ICircularList { this._array[this._getCyclicIndex(this._length)] = value; if (this._length === this._maxLength) { this._startIndex = ++this._startIndex % this._maxLength; - this.emitMayRemoveListeners('trim', 1); + this.onTrimEmitter.fire(1); } else { this._length++; } @@ -117,7 +123,7 @@ export class CircularList extends EventEmitter implements ICircularList { throw new Error('Can only recycle when the buffer is full'); } this._startIndex = ++this._startIndex % this._maxLength; - this.emitMayRemoveListeners('trim', 1); + this.onTrimEmitter.fire(1); return this._array[this._getCyclicIndex(this._length - 1)]!; } @@ -167,7 +173,7 @@ export class CircularList extends EventEmitter implements ICircularList { const countToTrim = (this._length + items.length) - this._maxLength; this._startIndex += countToTrim; this._length = this._maxLength; - this.emitMayRemoveListeners('trim', countToTrim); + this.onTrimEmitter.fire(countToTrim); } else { this._length += items.length; } @@ -183,7 +189,7 @@ export class CircularList extends EventEmitter implements ICircularList { } this._startIndex += count; this._length -= count; - this.emitMayRemoveListeners('trim', count); + this.onTrimEmitter.fire(count); } public shiftElements(start: number, count: number, offset: number): void { @@ -207,7 +213,7 @@ export class CircularList extends EventEmitter implements ICircularList { while (this._length > this._maxLength) { this._length--; this._startIndex++; - this.emitMayRemoveListeners('trim', 1); + this.onTrimEmitter.fire(1); } } } else { diff --git a/src/common/Types.ts b/src/common/Types.ts index 8a416bf1..c38b9c16 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -4,6 +4,8 @@ */ import { IEventEmitter } from 'xterm'; +import { IEvent, EventEmitter2 } from './EventEmitter2'; +import { IDeleteEvent, IInsertEvent } from './CircularList'; export type XtermListener = (...args: any[]) => void; @@ -21,11 +23,18 @@ export interface IKeyboardEvent { type: string; } -export interface ICircularList extends IEventEmitter { +export interface ICircularList { length: number; maxLength: number; isFull: boolean; + onDeleteEmitter: EventEmitter2; + onDelete: IEvent; + onInsertEmitter: EventEmitter2; + onInsert: IEvent; + onTrimEmitter: EventEmitter2; + onTrim: IEvent; + get(index: number): T | undefined; set(index: number, value: T): void; push(value: T): void; From 60c2b20d68bdf771b2f2a6c43eab6d5b7b619355 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 11:35:20 -0700 Subject: [PATCH 15/40] Replace EventEmitter usage in BufferSet --- src/BufferSet.ts | 15 +++++++++------ src/SelectionManager.ts | 3 +-- src/Types.ts | 5 ++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/BufferSet.ts b/src/BufferSet.ts index f84757d1..ffa5bc90 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -3,25 +3,28 @@ * @license MIT */ -import { ITerminal, IBufferSet } from './Types'; +import { ITerminal, IBufferSet, IBuffer } from './Types'; import { Buffer } from './Buffer'; -import { EventEmitter } from './common/EventEmitter'; +import { EventEmitter2, IEvent } from '../lib/common/EventEmitter2'; /** * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and * provides also utilities for working with them. */ -export class BufferSet extends EventEmitter implements IBufferSet { +export class BufferSet implements IBufferSet { private _normal: Buffer; private _alt: Buffer; private _activeBuffer: Buffer; + + private _onBufferActivate = new EventEmitter2<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>(); + public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; } + /** * Create a new BufferSet for the given terminal. * @param _terminal - The terminal the BufferSet will belong to */ constructor(private _terminal: ITerminal) { - super(); this._normal = new Buffer(this._terminal, true); this._normal.fillViewportRows(); @@ -68,7 +71,7 @@ export class BufferSet extends EventEmitter implements IBufferSet { // when activated. this._alt.clear(); this._activeBuffer = this._normal; - this.emit('activate', { + this._onBufferActivate.fire({ activeBuffer: this._normal, inactiveBuffer: this._alt }); @@ -87,7 +90,7 @@ export class BufferSet extends EventEmitter implements IBufferSet { this._alt.x = this._normal.x; this._alt.y = this._normal.y; this._activeBuffer = this._alt; - this.emit('activate', { + this._onBufferActivate.fire({ activeBuffer: this._alt, inactiveBuffer: this._normal }); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 702f6790..789a4bbf 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -140,7 +140,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager public initBuffersListeners(): void { this._trimListener = this._terminal.buffer.lines.onTrim(amount => this._onTrim(amount)); - this._terminal.buffers.on('activate', e => this._onBufferActivate(e)); + this._terminal.buffers.onBufferActivate(e => this._onBufferActivate(e)); } /** @@ -336,7 +336,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param amount The amount the buffer is being trimmed. */ private _onTrim(amount: number): void { - console.log('onTrim', amount); const needsRefresh = this._model.onTrim(amount); if (needsRefresh) { this.refresh(); diff --git a/src/Types.ts b/src/Types.ts index 10665f25..a4da0d54 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -8,6 +8,7 @@ import { IColorSet, IRenderer } from './renderer/Types'; import { IMouseZoneManager } from './ui/Types'; import { ICharset } from './core/Types'; import { ICircularList } from './common/Types'; +import { IEvent } from '../lib/common/EventEmitter2'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -302,11 +303,13 @@ export interface IBuffer { getWhitespaceCell(fg?: number, bg?: number): ICellData; } -export interface IBufferSet extends IEventEmitter { +export interface IBufferSet { alt: IBuffer; normal: IBuffer; active: IBuffer; + onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>; + activateNormalBuffer(): void; activateAltBuffer(fillAttr?: number): void; } From 60113fa155f5302ac7c6c9b2e662a6e3eb54269f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 11:38:55 -0700 Subject: [PATCH 16/40] Replace EventEmitter usage in CharMeasure --- src/Terminal.ts | 2 +- src/Types.ts | 3 +++ src/ui/CharMeasure.ts | 10 ++++++---- src/ui/TestUtils.test.ts | 1 + 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 1f05ec2e..991ede25 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -748,7 +748,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. this.register(addDisposableDomListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio))); - this.register(this.charMeasure.addDisposableListener('charsizechanged', () => this.renderer.onCharSizeChanged())); + this.register(this.charMeasure.onCharSizeChanged(() => this.renderer.onCharSizeChanged())); this.register(this.renderer.addDisposableListener('resize', (dimensions) => this.viewport.syncScrollArea())); this.selectionManager = new SelectionManager(this, this.charMeasure); diff --git a/src/Types.ts b/src/Types.ts index a4da0d54..38947af5 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -253,6 +253,9 @@ export interface IMouseHelper { export interface ICharMeasure { width: number; height: number; + + onCharSizeChanged: IEvent; + measure(options: ITerminalOptions): void; } diff --git a/src/ui/CharMeasure.ts b/src/ui/CharMeasure.ts index 0ac755ea..c24e82a0 100644 --- a/src/ui/CharMeasure.ts +++ b/src/ui/CharMeasure.ts @@ -4,22 +4,24 @@ */ import { ICharMeasure, ITerminalOptions } from '../Types'; -import { EventEmitter } from '../common/EventEmitter'; +import { EventEmitter2, IEvent } from '../../lib/common/EventEmitter2'; /** * Utility class that measures the size of a character. Measurements are done in * the DOM rather than with a canvas context because support for extracting the * height of characters is patchy across browsers. */ -export class CharMeasure extends EventEmitter implements ICharMeasure { +export class CharMeasure implements ICharMeasure { private _document: Document; private _parentElement: HTMLElement; private _measureElement: HTMLElement; private _width: number; private _height: number; + private _onCharSizeChanged = new EventEmitter2(); + public get onCharSizeChanged(): IEvent { return this._onCharSizeChanged.event; } + constructor(document: Document, parentElement: HTMLElement) { - super(); this._document = document; this._parentElement = parentElement; this._measureElement = this._document.createElement('span'); @@ -50,7 +52,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { if (this._width !== geometry.width || this._height !== adjustedHeight) { this._width = geometry.width; this._height = adjustedHeight; - this.emit('charsizechanged'); + this._onCharSizeChanged.fire(); } } } diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 2e23fe70..0e71f1a3 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -174,6 +174,7 @@ export class MockTerminal implements ITerminal { } export class MockCharMeasure implements ICharMeasure { + onCharSizeChanged: IEvent; width: number; height: number; measure(options: ITerminalOptions): void { From 7b9a718af01dc5edfefae4daae00383c8a19ea03 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 11:40:19 -0700 Subject: [PATCH 17/40] Fix some imports to point at ts files, not lib/ --- src/BufferSet.ts | 2 +- src/Types.ts | 2 +- src/public/Terminal.ts | 2 +- src/ui/CharMeasure.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BufferSet.ts b/src/BufferSet.ts index ffa5bc90..9eb14add 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -5,7 +5,7 @@ import { ITerminal, IBufferSet, IBuffer } from './Types'; import { Buffer } from './Buffer'; -import { EventEmitter2, IEvent } from '../lib/common/EventEmitter2'; +import { EventEmitter2, IEvent } from './common/EventEmitter2'; /** * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and diff --git a/src/Types.ts b/src/Types.ts index 38947af5..c9e51939 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -8,7 +8,7 @@ import { IColorSet, IRenderer } from './renderer/Types'; import { IMouseZoneManager } from './ui/Types'; import { ICharset } from './core/Types'; import { ICircularList } from './common/Types'; -import { IEvent } from '../lib/common/EventEmitter2'; +import { IEvent } from './common/EventEmitter2'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 60b275f3..53c3c693 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -7,7 +7,7 @@ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILink import { ITerminal } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; -import { IEvent } from '../../lib/common/EventEmitter2'; +import { IEvent } from '../common/EventEmitter2'; export class Terminal implements ITerminalApi { private _core: ITerminal; diff --git a/src/ui/CharMeasure.ts b/src/ui/CharMeasure.ts index c24e82a0..c37df312 100644 --- a/src/ui/CharMeasure.ts +++ b/src/ui/CharMeasure.ts @@ -4,7 +4,7 @@ */ import { ICharMeasure, ITerminalOptions } from '../Types'; -import { EventEmitter2, IEvent } from '../../lib/common/EventEmitter2'; +import { EventEmitter2, IEvent } from '../common/EventEmitter2'; /** * Utility class that measures the size of a character. Measurements are done in From 4e6f4be6cce7e9b99cc894fbce4b23869dc5ebe3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 4 Apr 2019 00:14:04 -0400 Subject: [PATCH 18/40] Convert Listener to an interface --- src/common/EventEmitter2.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/common/EventEmitter2.ts b/src/common/EventEmitter2.ts index a60c5836..71c0b72d 100644 --- a/src/common/EventEmitter2.ts +++ b/src/common/EventEmitter2.ts @@ -5,14 +5,16 @@ import { IDisposable } from './Types'; -type Listener = (e: T) => void; +interface IListener { + (e: T): void; +} export interface IEvent { (listener: (e: T) => any): IDisposable; } export class EventEmitter2 { - private _listeners: Listener[] = []; + private _listeners: IListener[] = []; private _event?: IEvent; public get event(): IEvent { @@ -36,7 +38,7 @@ export class EventEmitter2 { } public fire(data: T): void { - const queue: Listener[] = []; + const queue: IListener[] = []; for (let i = 0; i < this._listeners.length; i++) { queue.push(this._listeners[i]); } From f35dfb496aa46cc95dac6cd484aa5ad034ae78aa Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 4 Apr 2019 00:19:08 -0400 Subject: [PATCH 19/40] Use onKey in demo --- demo/client.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 7a601898..8a3e4d56 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -165,8 +165,9 @@ function runFakeTerminal(): void { term.writeln(''); term.prompt(); - term._core.register(term.addDisposableListener('key', (key, ev) => { - const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey; + term.onKey((e: { key: string, domEvent: KeyboardEvent }) => { + const ev = e.domEvent; + const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey; if (ev.keyCode === 13) { term.prompt(); @@ -176,9 +177,9 @@ function runFakeTerminal(): void { term.write('\b \b'); } } else if (printable) { - term.write(key); + term.write(e.key); } - })); + }); term._core.register(term.addDisposableListener('paste', (data, ev) => { term.write(data); From 3f9d90ecf65bf049b233d3fc6776b94c1c4425a8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 4 Apr 2019 00:23:12 -0400 Subject: [PATCH 20/40] Remove unnecessary paste listener from demo --- demo/client.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 8a3e4d56..f631a917 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -180,10 +180,6 @@ function runFakeTerminal(): void { term.write(e.key); } }); - - term._core.register(term.addDisposableListener('paste', (data, ev) => { - term.write(data); - })); } function initOptions(term: TerminalType): void { From 629238ae700b259ac8fe4e5e6303354648927806 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 4 Apr 2019 00:25:03 -0400 Subject: [PATCH 21/40] Use onResize in demo --- demo/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/client.ts b/demo/client.ts index f631a917..c73d81dd 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -86,7 +86,7 @@ function createTerminal(): void { windowsMode: isWindows } as ITerminalOptions); window.term = term; // Expose `term` to window for debugging purposes - term.on('resize', (size: { cols: number, rows: number }) => { + term.onResize((size: { cols: number, rows: number }) => { if (!pid) { return; } From 2f8d49c74df812e8c4a5a90b0f5b44c5d3dcb2d9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 4 Apr 2019 00:45:57 -0400 Subject: [PATCH 22/40] Convert internal key event usages --- src/AccessibilityManager.ts | 2 +- src/Terminal.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index 877676c9..a8e0ba1b 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -79,7 +79,7 @@ export class AccessibilityManager extends Disposable { this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char))); this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n'))); this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount))); - this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar))); + this.register(this._terminal.onKey(e => this._onKey(e.key))); this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion())); // TODO: Maybe renderer should fire an event on terminal when the characters change and that // should be listened to instead? That would mean that the order of events are always diff --git a/src/Terminal.ts b/src/Terminal.ts index f6a987f7..5d2e96de 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -266,9 +266,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.on('data', e => this._onInput.fire(e)); this.on('title', e => this._onTitleChange.fire(e)); this.on('scroll', e => this._onScroll.fire(e)); - this.on('key', e => this._onKey.fire(e)); this.on('refresh', e => this._onRender.fire(e)); this.on('resize', e => this._onResize.fire(e)); + + // TODO: Remove these in v4 + // Fire old style events from new emitters + this.onKey(e => this.emit('key', e.key, e.domEvent)); } public dispose(): void { @@ -1615,7 +1618,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } this.emit('keydown', event); - this.emit('key', result.key, event); + this._onKey.fire({ key: result.key, domEvent: event }); this.showCursor(); this.handler(result.key); @@ -1694,7 +1697,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II key = String.fromCharCode(key); this.emit('keypress', key, ev); - this.emit('key', key, ev); + this._onKey.fire({ key, domEvent: ev }); this.showCursor(); this.handler(key); From 7e36bf47c679f639027bd693862bed83e1793e1f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 6 Apr 2019 15:21:04 -0400 Subject: [PATCH 23/40] Have undefined | true mean success --- src/EscapeSequenceParser.ts | 8 +- src/InputHandler.ts | 179 +++++++++++++----------------------- src/Types.ts | 108 +++++++++++----------- 3 files changed, 122 insertions(+), 173 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 7b65624d..197a3657 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -353,7 +353,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (this._oscHandlers[ident] === undefined) { this._oscHandlers[ident] = []; } - const handlerList = this._oscHandlers[ident]; + const handlerList = this._oscHandlers[ident]; handlerList.push(callback); return { dispose: () => { @@ -505,7 +505,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP const handlers = this._csiHandlers[code]; let j = handlers ? handlers.length - 1 : -1; for (; j >= 0; j--) { - if (handlers[j](params, collect)) { + // undefined or true means success and to stop bubbling + if (handlers[j](params, collect) !== false) { break; } } @@ -590,7 +591,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP const handlers = this._oscHandlers[identifier]; let j = handlers ? handlers.length - 1 : -1; for (; j >= 0; j--) { - if (handlers[j](content)) { + // undefined or true means success and to stop bubbling + if (handlers[j](content) !== false) { break; } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index e8d6d906..33e2db56 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -439,16 +439,15 @@ export class InputHandler extends Disposable implements IInputHandler { * BEL * Bell (Ctrl-G). */ - public bell(): boolean { + public bell(): void { this._terminal.bell(); - return true; } /** * LF * Line Feed or New Line (NL). (LF is Ctrl-J). */ - public lineFeed(): boolean { + public lineFeed(): void { // make buffer local for faster access const buffer = this._terminal.buffer; @@ -470,40 +469,36 @@ export class InputHandler extends Disposable implements IInputHandler { * @event linefeed */ this._terminal.emit('linefeed'); - return true; } /** * CR * Carriage Return (Ctrl-M). */ - public carriageReturn(): boolean { + public carriageReturn(): void { this._terminal.buffer.x = 0; - return true; } /** * BS * Backspace (Ctrl-H). */ - public backspace(): boolean { + public backspace(): void { if (this._terminal.buffer.x > 0) { this._terminal.buffer.x--; } - return true; } /** * TAB * Horizontal Tab (HT) (Ctrl-I). */ - public tab(): boolean { + public tab(): void { const originalX = this._terminal.buffer.x; this._terminal.buffer.x = this._terminal.buffer.nextStop(); if (this._terminal.options.screenReaderMode) { this._terminal.emit('a11y.tab', this._terminal.buffer.x - originalX); } - return true; } /** @@ -511,9 +506,8 @@ export class InputHandler extends Disposable implements IInputHandler { * Shift Out (Ctrl-N) -> Switch to Alternate Character Set. This invokes the * G1 character set. */ - public shiftOut(): boolean { + public shiftOut(): void { this._terminal.setgLevel(1); - return true; } /** @@ -521,30 +515,28 @@ export class InputHandler extends Disposable implements IInputHandler { * Shift In (Ctrl-O) -> Switch to Standard Character Set. This invokes the G0 * character set (the default). */ - public shiftIn(): boolean { + public shiftIn(): void { this._terminal.setgLevel(0); - return true; } /** * CSI Ps @ * Insert Ps (Blank) Character(s) (default = 1) (ICH). */ - public insertChars(params: number[]): boolean { + public insertChars(params: number[]): void { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells( this._terminal.buffer.x, params[0] || 1, this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); this._terminal.updateRange(this._terminal.buffer.y); - return true; } /** * CSI Ps A * Cursor Up Ps Times (default = 1) (CUU). */ - public cursorUp(params: number[]): boolean { + public cursorUp(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -553,14 +545,13 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.y < 0) { this._terminal.buffer.y = 0; } - return true; } /** * CSI Ps B * Cursor Down Ps Times (default = 1) (CUD). */ - public cursorDown(params: number[]): boolean { + public cursorDown(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -573,14 +564,13 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x--; } - return true; } /** * CSI Ps C * Cursor Forward Ps Times (default = 1) (CUF). */ - public cursorForward(params: number[]): boolean { + public cursorForward(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -589,14 +579,13 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } - return true; } /** * CSI Ps D * Cursor Backward Ps Times (default = 1) (CUB). */ - public cursorBackward(params: number[]): boolean { + public cursorBackward(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -609,7 +598,6 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x < 0) { this._terminal.buffer.x = 0; } - return true; } /** @@ -617,7 +605,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Next Line Ps Times (default = 1) (CNL). * same as CSI Ps B ? */ - public cursorNextLine(params: number[]): boolean { + public cursorNextLine(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -627,7 +615,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.y = this._terminal.rows - 1; } this._terminal.buffer.x = 0; - return true; } @@ -636,7 +623,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Preceding Line Ps Times (default = 1) (CNL). * reuse CSI Ps A ? */ - public cursorPrecedingLine(params: number[]): boolean { + public cursorPrecedingLine(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -646,7 +633,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.y = 0; } this._terminal.buffer.x = 0; - return true; } @@ -654,20 +640,19 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps G * Cursor Character Absolute [column] (default = [row,1]) (CHA). */ - public cursorCharAbsolute(params: number[]): boolean { + public cursorCharAbsolute(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; } this._terminal.buffer.x = param - 1; - return true; } /** * CSI Ps ; Ps H * Cursor Position [row;column] (default = [1,1]) (CUP). */ - public cursorPosition(params: number[]): boolean { + public cursorPosition(params: number[]): void { let col: number; let row: number = params[0] - 1; @@ -691,19 +676,17 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.x = col; this._terminal.buffer.y = row; - return true; } /** * CSI Ps I * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). */ - public cursorForwardTab(params: number[]): boolean { + public cursorForwardTab(params: number[]): void { let param = params[0] || 1; while (param--) { this._terminal.buffer.x = this._terminal.buffer.nextStop(); } - return true; } /** @@ -746,7 +729,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 -> Selective Erase Above. * Ps = 2 -> Selective Erase All. */ - public eraseInDisplay(params: number[]): boolean { + public eraseInDisplay(params: number[]): void { let j; switch (params[0]) { case 0: @@ -792,7 +775,6 @@ export class InputHandler extends Disposable implements IInputHandler { } break; } - return true; } /** @@ -806,7 +788,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 -> Selective Erase to Left. * Ps = 2 -> Selective Erase All. */ - public eraseInLine(params: number[]): boolean { + public eraseInLine(params: number[]): void { switch (params[0]) { case 0: this._eraseInBufferLine(this._terminal.buffer.y, this._terminal.buffer.x, this._terminal.cols); @@ -819,14 +801,13 @@ export class InputHandler extends Disposable implements IInputHandler { break; } this._terminal.updateRange(this._terminal.buffer.y); - return true; } /** * CSI Ps L * Insert Ps Line(s) (default = 1) (IL). */ - public insertLines(params: number[]): boolean { + public insertLines(params: number[]): void { let param: number = params[0]; if (param < 1) { param = 1; @@ -849,14 +830,13 @@ export class InputHandler extends Disposable implements IInputHandler { // this.maxRange(); this._terminal.updateRange(buffer.y); this._terminal.updateRange(buffer.scrollBottom); - return true; } /** * CSI Ps M * Delete Ps Line(s) (default = 1) (DL). */ - public deleteLines(params: number[]): boolean { + public deleteLines(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -880,27 +860,25 @@ export class InputHandler extends Disposable implements IInputHandler { // this.maxRange(); this._terminal.updateRange(buffer.y); this._terminal.updateRange(buffer.scrollBottom); - return true; } /** * CSI Ps P * Delete Ps Character(s) (default = 1) (DCH). */ - public deleteChars(params: number[]): boolean { + public deleteChars(params: number[]): void { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells( this._terminal.buffer.x, params[0] || 1, this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); this._terminal.updateRange(this._terminal.buffer.y); - return true; } /** * CSI Ps S Scroll up Ps lines (default = 1) (SU). */ - public scrollUp(params: number[]): boolean { + public scrollUp(params: number[]): void { let param = params[0] || 1; // make buffer local for faster access @@ -913,13 +891,12 @@ export class InputHandler extends Disposable implements IInputHandler { // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); this._terminal.updateRange(buffer.scrollBottom); - return true; } /** * CSI Ps T Scroll down Ps lines (default = 1) (SD). */ - public scrollDown(params: number[], collect?: string): boolean { + public scrollDown(params: number[], collect?: string): void { if (params.length < 2 && !collect) { let param = params[0] || 1; @@ -934,26 +911,24 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.scrollTop); this._terminal.updateRange(buffer.scrollBottom); } - return true; } /** * CSI Ps X * Erase Ps Character(s) (default = 1) (ECH). */ - public eraseChars(params: number[]): boolean { + public eraseChars(params: number[]): void { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells( this._terminal.buffer.x, this._terminal.buffer.x + (params[0] || 1), this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); - return true; } /** * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). */ - public cursorBackwardTab(params: number[]): boolean { + public cursorBackwardTab(params: number[]): void { let param = params[0] || 1; // make buffer local for faster access @@ -962,14 +937,13 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.x = buffer.prevStop(); } - return true; } /** * CSI Pm ` Character Position Absolute * [column] (default = [row,1]) (HPA). */ - public charPosAbsolute(params: number[]): boolean { + public charPosAbsolute(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -978,7 +952,6 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } - return true; } /** @@ -986,7 +959,7 @@ export class InputHandler extends Disposable implements IInputHandler { * [columns] (default = [row,col+1]) (HPR) * reuse CSI Ps C ? */ - public hPositionRelative(params: number[]): boolean { + public hPositionRelative(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -995,13 +968,12 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } - return true; } /** * CSI Ps b Repeat the preceding graphic character Ps times (REP). */ - public repeatPrecedingCharacter(params: number[]): boolean { + public repeatPrecedingCharacter(params: number[]): void { // make buffer local for faster access const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); @@ -1011,7 +983,6 @@ export class InputHandler extends Disposable implements IInputHandler { (this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR) ); // FIXME: no updateRange here? - return true; } /** @@ -1051,9 +1022,9 @@ export class InputHandler extends Disposable implements IInputHandler { * xterm/charproc.c - line 2012, for more information. * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) */ - public sendDeviceAttributes(params: number[], collect?: string): boolean { + public sendDeviceAttributes(params: number[], collect?: string): void { if (params[0] > 0) { - return true; + return; } if (!collect) { @@ -1078,14 +1049,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.handler(C0.ESC + '[>83;40003;0c'); } } - return true; } /** * CSI Pm d Vertical Position Absolute (VPA) * [row] (default = [1,column]) */ - public linePosAbsolute(params: number[]): boolean { + public linePosAbsolute(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -1094,7 +1064,6 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.y >= this._terminal.rows) { this._terminal.buffer.y = this._terminal.rows - 1; } - return true; } /** @@ -1102,7 +1071,7 @@ export class InputHandler extends Disposable implements IInputHandler { * [rows] (default = [row+1,column]) * reuse CSI Ps B ? */ - public vPositionRelative(params: number[]): boolean { + public vPositionRelative(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -1115,7 +1084,6 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x--; } - return true; } /** @@ -1123,7 +1091,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Horizontal and Vertical Position [row;column] (default = * [1,1]) (HVP). */ - public hVPosition(params: number[]): boolean { + public hVPosition(params: number[]): void { if (params[0] < 1) params[0] = 1; if (params[1] < 1) params[1] = 1; @@ -1136,7 +1104,6 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.buffer.x >= this._terminal.cols) { this._terminal.buffer.x = this._terminal.cols - 1; } - return true; } /** @@ -1147,14 +1114,13 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 2 -> Clear Stops on Line. * http://vt100.net/annarbor/aaa-ug/section6.html */ - public tabClear(params: number[]): boolean { + public tabClear(params: number[]): void { const param = params[0]; if (param <= 0) { delete this._terminal.buffer.tabs[this._terminal.buffer.x]; } else if (param === 3) { this._terminal.buffer.tabs = {}; } - return true; } /** @@ -1243,13 +1209,13 @@ export class InputHandler extends Disposable implements IInputHandler { * Modes: * http: *vt100.net/docs/vt220-rm/chapter4.html */ - public setMode(params: number[], collect?: string): boolean { + public setMode(params: number[], collect?: string): void { if (params.length > 1) { for (let i = 0; i < params.length; i++) { this.setMode([params[i]]); } - return true; + return; } if (!collect) { @@ -1364,7 +1330,6 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } - return true; } /** @@ -1449,13 +1414,13 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. * Ps = 2 0 0 4 -> Reset bracketed paste mode. */ - public resetMode(params: number[], collect?: string): boolean { + public resetMode(params: number[], collect?: string): void { if (params.length > 1) { for (let i = 0; i < params.length; i++) { this.resetMode([params[i]]); } - return true; + return; } if (!collect) { @@ -1547,7 +1512,6 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } - return true; } /** @@ -1615,11 +1579,11 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 4 8 ; 5 ; Ps -> Set background color to the second * Ps. */ - public charAttributes(params: number[]): boolean { + public charAttributes(params: number[]): void { // Optimize a single SGR0. if (params.length === 1 && params[0] === 0) { this._terminal.curAttr = DEFAULT_ATTR; - return true; + return; } const l = params.length; @@ -1739,8 +1703,6 @@ export class InputHandler extends Disposable implements IInputHandler { } this._terminal.curAttr = (flags << 18) | (fg << 9) | bg; - - return true; } /** @@ -1766,7 +1728,7 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI ? 5 3 n Locator available, if compiled-in, or * CSI ? 5 0 n No Locator, if not. */ - public deviceStatus(params: number[], collect?: string): boolean { + public deviceStatus(params: number[], collect?: string): void { if (!collect) { switch (params[0]) { case 5: @@ -1808,14 +1770,13 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } - return true; } /** * CSI ! p Soft terminal reset (DECSTR). * http://vt100.net/docs/vt220-rm/table4-10.html */ - public softReset(params: number[], collect?: string): boolean { + public softReset(params: number[], collect?: string): void { if (collect === '!') { this._terminal.cursorHidden = false; this._terminal.insertMode = false; @@ -1834,7 +1795,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.glevel = 0; // ?? this._terminal.charsets = [null]; // ?? } - return true; } /** @@ -1847,7 +1807,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 5 -> blinking bar (xterm). * Ps = 6 -> steady bar (xterm). */ - public setCursorStyle(params?: number[], collect?: string): boolean { + public setCursorStyle(params?: number[], collect?: string): void { if (collect === ' ') { const param = params[0] < 1 ? 1 : params[0]; switch (param) { @@ -1867,7 +1827,6 @@ export class InputHandler extends Disposable implements IInputHandler { const isBlinking = param % 2 === 1; this._terminal.setOption('cursorBlink', isBlinking); } - return true; } /** @@ -1876,15 +1835,14 @@ export class InputHandler extends Disposable implements IInputHandler { * dow) (DECSTBM). * CSI ? Pm r */ - public setScrollRegion(params: number[], collect?: string): boolean { + public setScrollRegion(params: number[], collect?: string): void { if (collect) { - return true; + return; } this._terminal.buffer.scrollTop = (params[0] || 1) - 1; this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1; this._terminal.buffer.x = 0; this._terminal.buffer.y = 0; - return true; } @@ -1893,11 +1851,10 @@ export class InputHandler extends Disposable implements IInputHandler { * ESC 7 * Save cursor (ANSI.SYS). */ - public saveCursor(params: number[]): boolean { + public saveCursor(params: number[]): void { this._terminal.buffer.savedX = this._terminal.buffer.x; this._terminal.buffer.savedY = this._terminal.buffer.y; this._terminal.buffer.savedCurAttr = this._terminal.curAttr; - return true; } @@ -1906,11 +1863,10 @@ export class InputHandler extends Disposable implements IInputHandler { * ESC 8 * Restore cursor (ANSI.SYS). */ - public restoreCursor(params: number[]): boolean { + public restoreCursor(params: number[]): void { this._terminal.buffer.x = this._terminal.buffer.savedX || 0; this._terminal.buffer.y = this._terminal.buffer.savedY || 0; this._terminal.curAttr = this._terminal.buffer.savedCurAttr || DEFAULT_ATTR; - return true; } @@ -1919,9 +1875,8 @@ export class InputHandler extends Disposable implements IInputHandler { * OSC 2; ST (set window title) * Proxy to set window title. Icon name is not supported. */ - public setTitle(data: string): boolean { + public setTitle(data: string): void { this._terminal.handleTitle(data); - return true; } /** @@ -1930,10 +1885,9 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL) * Moves cursor to first position on next line. */ - public nextLine(): boolean { + public nextLine(): void { this._terminal.buffer.x = 0; this.index(); - return true; } /** @@ -1941,13 +1895,12 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html) * Enables the numeric keypad to send application sequences to the host. */ - public keypadApplicationMode(): boolean { + public keypadApplicationMode(): void { this._terminal.log('Serial port requested application keypad.'); this._terminal.applicationKeypad = true; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } - return true; } /** @@ -1955,13 +1908,12 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html) * Enables the keypad to send numeric characters to the host. */ - public keypadNumericMode(): boolean { + public keypadNumericMode(): void { this._terminal.log('Switching back to normal keypad.'); this._terminal.applicationKeypad = false; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } - return true; } /** @@ -1970,10 +1922,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Select default character set. UTF-8 is not supported (string are unicode anyways) * therefore ESC % G does the same. */ - public selectDefaultCharset(): boolean { + public selectDefaultCharset(): void { this._terminal.setgLevel(0); this._terminal.setgCharset(0, DEFAULT_CHARSET); // US (default) - return true; } /** @@ -1992,15 +1943,16 @@ export class InputHandler extends Disposable implements IInputHandler { * ESC / C * Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported? */ - public selectCharset(collectAndFlag: string): boolean { + public selectCharset(collectAndFlag: string): void { if (collectAndFlag.length !== 2) { - return this.selectDefaultCharset(); + this.selectDefaultCharset(); + return; } if (collectAndFlag[0] === '/') { - return true; // TODO: Is this supported? + return; // TODO: Is this supported? } this._terminal.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); - return true; + return; } /** @@ -2009,9 +1961,8 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html) * Moves the cursor down one line in the same column. */ - public index(): boolean { + public index(): void { this._terminal.index(); // TODO: save to move from terminal? - return true; } /** @@ -2021,9 +1972,8 @@ export class InputHandler extends Disposable implements IInputHandler { * Sets a horizontal tab stop at the column position indicated by * the value of the active column when the terminal receives an HTS. */ - public tabSet(): boolean { + public tabSet(): void { this._terminal.tabSet(); // TODO: save to move from terminal? - return true; } /** @@ -2033,9 +1983,8 @@ export class InputHandler extends Disposable implements IInputHandler { * Moves the cursor up one line in the same column. If the cursor is at the top margin, * the page scrolls down. */ - public reverseIndex(): boolean { + public reverseIndex(): void { this._terminal.reverseIndex(); // TODO: save to move from terminal? - return true; } /** @@ -2043,10 +1992,9 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html) * Reset to initial state. */ - public reset(): boolean { + public reset(): void { this._parser.reset(); this._terminal.reset(); // TODO: save to move from terminal? - return true; } /** @@ -2059,8 +2007,7 @@ export class InputHandler extends Disposable implements IInputHandler { * When you use a locking shift, the character set remains in GL or GR until * you use another locking shift. (partly supported) */ - public setgLevel(level: number): boolean { + public setgLevel(level: number): void { this._terminal.setgLevel(level); // TODO: save to move from terminal? - return true; } } diff --git a/src/Types.ts b/src/Types.ts index a21ac59e..cbb8e502 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -115,73 +115,73 @@ export interface IInputHandler { addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; - /** C0 BEL */ bell(): boolean; - /** C0 LF */ lineFeed(): boolean; - /** C0 CR */ carriageReturn(): boolean; - /** C0 BS */ backspace(): boolean; - /** C0 HT */ tab(): boolean; - /** C0 SO */ shiftOut(): boolean; - /** C0 SI */ shiftIn(): boolean; + /** C0 BEL */ bell(): void; + /** C0 LF */ lineFeed(): void; + /** C0 CR */ carriageReturn(): void; + /** C0 BS */ backspace(): void; + /** C0 HT */ tab(): void; + /** C0 SO */ shiftOut(): void; + /** C0 SI */ shiftIn(): void; - /** CSI @ */ insertChars(params?: number[]): boolean; - /** CSI A */ cursorUp(params?: number[]): boolean; - /** CSI B */ cursorDown(params?: number[]): boolean; - /** CSI C */ cursorForward(params?: number[]): boolean; - /** CSI D */ cursorBackward(params?: number[]): boolean; - /** CSI E */ cursorNextLine(params?: number[]): boolean; - /** CSI F */ cursorPrecedingLine(params?: number[]): boolean; - /** CSI G */ cursorCharAbsolute(params?: number[]): boolean; - /** CSI H */ cursorPosition(params?: number[]): boolean; - /** CSI I */ cursorForwardTab(params?: number[]): boolean; - /** CSI J */ eraseInDisplay(params?: number[]): boolean; - /** CSI K */ eraseInLine(params?: number[]): boolean; - /** CSI L */ insertLines(params?: number[]): boolean; - /** CSI M */ deleteLines(params?: number[]): boolean; - /** CSI P */ deleteChars(params?: number[]): boolean; - /** CSI S */ scrollUp(params?: number[]): boolean; - /** CSI T */ scrollDown(params?: number[], collect?: string): boolean; - /** CSI X */ eraseChars(params?: number[]): boolean; - /** CSI Z */ cursorBackwardTab(params?: number[]): boolean; - /** CSI ` */ charPosAbsolute(params?: number[]): boolean; - /** CSI a */ hPositionRelative(params?: number[]): boolean; - /** CSI b */ repeatPrecedingCharacter(params?: number[]): boolean; - /** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): boolean; - /** CSI d */ linePosAbsolute(params?: number[]): boolean; - /** CSI e */ vPositionRelative(params?: number[]): boolean; - /** CSI f */ hVPosition(params?: number[]): boolean; - /** CSI g */ tabClear(params?: number[]): boolean; - /** CSI h */ setMode(params?: number[], collect?: string): boolean; - /** CSI l */ resetMode(params?: number[], collect?: string): boolean; - /** CSI m */ charAttributes(params?: number[]): boolean; - /** CSI n */ deviceStatus(params?: number[], collect?: string): boolean; - /** CSI p */ softReset(params?: number[], collect?: string): boolean; - /** CSI q */ setCursorStyle(params?: number[], collect?: string): boolean; - /** CSI r */ setScrollRegion(params?: number[], collect?: string): boolean; - /** CSI s */ saveCursor(params?: number[]): boolean; - /** CSI u */ restoreCursor(params?: number[]): boolean; + /** CSI @ */ insertChars(params?: number[]): void; + /** CSI A */ cursorUp(params?: number[]): void; + /** CSI B */ cursorDown(params?: number[]): void; + /** CSI C */ cursorForward(params?: number[]): void; + /** CSI D */ cursorBackward(params?: number[]): void; + /** CSI E */ cursorNextLine(params?: number[]): void; + /** CSI F */ cursorPrecedingLine(params?: number[]): void; + /** CSI G */ cursorCharAbsolute(params?: number[]): void; + /** CSI H */ cursorPosition(params?: number[]): void; + /** CSI I */ cursorForwardTab(params?: number[]): void; + /** CSI J */ eraseInDisplay(params?: number[]): void; + /** CSI K */ eraseInLine(params?: number[]): void; + /** CSI L */ insertLines(params?: number[]): void; + /** CSI M */ deleteLines(params?: number[]): void; + /** CSI P */ deleteChars(params?: number[]): void; + /** CSI S */ scrollUp(params?: number[]): void; + /** CSI T */ scrollDown(params?: number[], collect?: string): void; + /** CSI X */ eraseChars(params?: number[]): void; + /** CSI Z */ cursorBackwardTab(params?: number[]): void; + /** CSI ` */ charPosAbsolute(params?: number[]): void; + /** CSI a */ hPositionRelative(params?: number[]): void; + /** CSI b */ repeatPrecedingCharacter(params?: number[]): void; + /** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): void; + /** CSI d */ linePosAbsolute(params?: number[]): void; + /** CSI e */ vPositionRelative(params?: number[]): void; + /** CSI f */ hVPosition(params?: number[]): void; + /** CSI g */ tabClear(params?: number[]): void; + /** CSI h */ setMode(params?: number[], collect?: string): void; + /** CSI l */ resetMode(params?: number[], collect?: string): void; + /** CSI m */ charAttributes(params?: number[]): void; + /** CSI n */ deviceStatus(params?: number[], collect?: string): void; + /** CSI p */ softReset(params?: number[], collect?: string): void; + /** CSI q */ setCursorStyle(params?: number[], collect?: string): void; + /** CSI r */ setScrollRegion(params?: number[], collect?: string): void; + /** CSI s */ saveCursor(params?: number[]): void; + /** CSI u */ restoreCursor(params?: number[]): void; /** OSC 0 - OSC 2 */ setTitle(data: string): boolean; - /** ESC E */ nextLine(): boolean; - /** ESC = */ keypadApplicationMode(): boolean; - /** ESC > */ keypadNumericMode(): boolean; + OSC 2 */ setTitle(data: string): void; + /** ESC E */ nextLine(): void; + /** ESC = */ keypadApplicationMode(): void; + /** ESC > */ keypadNumericMode(): void; /** ESC % G - ESC % @ */ selectDefaultCharset(): boolean; + ESC % @ */ selectDefaultCharset(): void; /** ESC ( C ESC ) C ESC * C ESC + C ESC - C ESC . C - ESC / C */ selectCharset(collectAndFlag: string): boolean; - /** ESC D */ index(): boolean; - /** ESC H */ tabSet(): boolean; - /** ESC M */ reverseIndex(): boolean; - /** ESC c */ reset(): boolean; + ESC / C */ selectCharset(collectAndFlag: string): void; + /** ESC D */ index(): void; + /** ESC H */ tabSet(): void; + /** ESC M */ reverseIndex(): void; + /** ESC c */ reset(): void; /** ESC n ESC o ESC | ESC } - ESC ~ */ setgLevel(level: number): boolean; + ESC ~ */ setgLevel(level: number): void; } export interface ILinkMatcher { From 625e168e1be9149f01a6ab859c1c43a7cb6336de Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 7 Apr 2019 14:02:11 -0400 Subject: [PATCH 24/40] Convert title and resize events --- src/Terminal.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index cb0e3050..b346e851 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -265,14 +265,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.on('linefeed', () => this._onLineFeed.fire()); this.on('selection', () => this._onSelectionChange.fire()); this.on('data', e => this._onInput.fire(e)); - this.on('title', e => this._onTitleChange.fire(e)); this.on('scroll', e => this._onScroll.fire(e)); this.on('refresh', e => this._onRender.fire(e)); - this.on('resize', e => this._onResize.fire(e)); // TODO: Remove these in v4 // Fire old style events from new emitters this.onKey(e => this.emit('key', e.key, e.domEvent)); + this.onResize(e => this.emit('resize', e)); + this.onTitleChange(e => this.emit('title', e)); } public dispose(): void { @@ -770,7 +770,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.register(this.viewport); this.register(this.addDisposableListener('cursormove', () => this.renderer.onCursorMove())); - this.register(this.addDisposableListener('resize', () => this.renderer.onResize(this.cols, this.rows))); + this.register(this.onResize(() => this.renderer.onResize(this.cols, this.rows))); this.register(this.addDisposableListener('blur', () => this.renderer.onBlur())); this.register(this.addDisposableListener('focus', () => this.renderer.onFocus())); this.register(this.addDisposableListener('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio))); @@ -1778,7 +1778,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } this.refresh(0, this.rows - 1); - this.emit('resize', {cols: x, rows: y}); + this._onResize.fire({ cols: x, rows: y }); } /** @@ -1859,13 +1859,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param title The title to populate in the event. */ public handleTitle(title: string): void { - /** - * This event is emitted when the title of the terminal is changed - * from inside the terminal. The parameter is the new title. - * - * @event title - */ - this.emit('title', title); + this._onTitleChange.fire(title); } /** From 6ff81c43f64eff514f009879f360fa7c277d997a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 7 Apr 2019 14:09:31 -0400 Subject: [PATCH 25/40] Convert marker dispose, selection manager newselection --- src/Buffer.ts | 8 ++++++-- src/SelectionManager.ts | 12 ++++++++---- src/Terminal.ts | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index c1dc2fec..e84908d0 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -10,6 +10,7 @@ import { IMarker } from 'xterm'; import { BufferLine, CellData, AttributeData } from './BufferLine'; import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; +import { EventEmitter2, IEvent } from './common/EventEmitter2'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); @@ -615,7 +616,7 @@ export class Buffer implements IBuffer { marker.line -= event.amount; } })); - marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker))); + marker.register(marker.onDispose(() => this._removeMarker(marker))); return marker; } @@ -636,6 +637,9 @@ export class Marker extends EventEmitter implements IMarker { public get id(): number { return this._id; } + private _onDispose = new EventEmitter2(); + public get onDispose(): IEvent { return this._onDispose.event; } + constructor( public line: number ) { @@ -648,7 +652,7 @@ export class Marker extends EventEmitter implements IMarker { } this.isDisposed = true; // Emit before super.dispose such that dispose listeners get a change to react - this.emit('dispose'); + this._onDispose.fire(); super.dispose(); } } diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 366dc767..54499428 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -12,6 +12,7 @@ import { SelectionModel } from './SelectionModel'; import { AltClickHandler } from './handlers/AltClickHandler'; import { CellData } from './BufferLine'; import { IDisposable } from 'xterm'; +import { EventEmitter2, IEvent } from './common/EventEmitter2'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -107,6 +108,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _mouseDownTimeStamp: number; + private _onNewMouseSelection = new EventEmitter2(); + public get onNewMouseSelection(): IEvent { return this._onNewMouseSelection.event; } + constructor( private _terminal: ITerminal, private _charMeasure: CharMeasure @@ -244,10 +248,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager /** * Queues a refresh, redrawing the selection on the next opportunity. - * @param isNewSelection Whether the selection should be registered as a new + * @param isNewMouseSelection Whether the selection should be registered as a new * selection on Linux. */ - public refresh(isNewSelection?: boolean): void { + public refresh(isNewMouseSelection?: boolean): void { // Queue the refresh for the renderer if (!this._refreshAnimationFrame) { this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh()); @@ -255,10 +259,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // If the platform is Linux and the refresh call comes from a mouse event, // we need to update the selection for middle click to paste selection. - if (Browser.isLinux && isNewSelection) { + if (Browser.isLinux && isNewMouseSelection) { const selectionText = this.selectionText; if (selectionText.length) { - this.emit('newselection', this.selectionText); + this._onNewMouseSelection.fire(this.selectionText); } } } diff --git a/src/Terminal.ts b/src/Terminal.ts index b346e851..c92f5da2 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -783,7 +783,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.selectionManager = new SelectionManager(this, this.charMeasure); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e))); this.register(this.selectionManager.addDisposableListener('refresh', data => this.renderer.onSelectionChanged(data.start, data.end, data.columnSelectMode))); - this.register(this.selectionManager.addDisposableListener('newselection', text => { + this.register(this.selectionManager.onNewMouseSelection(text => { // If there's a new selection, put it into the textarea, focus and select it // in order to register it as a selection on the OS. This event is fired // only on Linux to enable middle click to paste selection. From dee4429151c2789f9208977c737d09d6aa5fee38 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 11:11:16 -0400 Subject: [PATCH 26/40] Remove unneeded interfaces --- src/Types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Types.ts b/src/Types.ts index 7967662a..6e0108ae 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -111,8 +111,6 @@ export interface ICompositionHelper { export interface IInputHandler { parse(data: string): void; print(data: Uint32Array, start: number, end: number): void; - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; From 8b5b64605e0c0168fb0c1b529eee52b0a9d74fea Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 19:53:22 -0400 Subject: [PATCH 27/40] Start find previous from the current viewport --- src/addons/search/SearchHelper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 57a8c0a8..3dfc8891 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -115,8 +115,8 @@ export class SearchHelper implements ISearchHelper { } const isReverseSearch = true; - let startRow = this._terminal.rows - 1; - let startCol: number = this._terminal.cols; + let startRow = this._terminal._core.buffer.ydisp + this._terminal.rows - 1; + let startCol = this._terminal.cols; if (selectionManager.selectionStart) { // Start from the selection start if there is a selection From 8e5d372b81141cbe1af2c3ca842dec52ae16ed4a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 19:31:59 -0700 Subject: [PATCH 28/40] Convert cursormove to EventEmitter2 --- src/InputHandler.ts | 6 +++++- src/Terminal.test.ts | 10 ++++++++++ src/Terminal.ts | 6 ++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 95eab4a3..d0471286 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -15,6 +15,7 @@ import { Disposable } from './common/Lifecycle'; import { concat } from './common/TypedArrayUtils'; import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder'; import { CellData, Attributes, FgFlags, BgFlags, AttributeData } from './BufferLine'; +import { EventEmitter2, IEvent } from './common/EventEmitter2'; /** * Map collect to glevel. Used in `selectCharset`. @@ -106,6 +107,9 @@ export class InputHandler extends Disposable implements IInputHandler { private _stringDecoder: StringToUtf32 = new StringToUtf32(); private _workCell: CellData = new CellData(); + private _onCursorMove = new EventEmitter2(); + public get onCursorMove(): IEvent { return this._onCursorMove.event; } + constructor( protected _terminal: IInputHandlingTerminal, private _parser: IEscapeSequenceParser = new EscapeSequenceParser()) @@ -305,7 +309,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer = this._terminal.buffer; if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { - this._terminal.emit('cursormove'); + this._onCursorMove.fire(); } } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index f4b717d8..2422c038 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -85,6 +85,16 @@ describe('term.js addons', () => { }); }); + describe('cursormove', () => { + it('should emit a cursormove event', (done) => { + term.on('cursormove', () => { + done(); + }); + + term.write('foo'); + }); + }); + describe(`keypress (including 'key' event)`, () => { it('should receive a string and event object', (done) => { let steps = 0; diff --git a/src/Terminal.ts b/src/Terminal.ts index c92f5da2..64178c74 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -261,7 +261,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._setup(); // TODO: Replace EventEmitter with EventEmitter2 internally - this.on('cursormove', () => this._onCursorMove.fire()); this.on('linefeed', () => this._onLineFeed.fire()); this.on('selection', () => this._onSelectionChange.fire()); this.on('data', e => this._onInput.fire(e)); @@ -270,6 +269,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: Remove these in v4 // Fire old style events from new emitters + this.onCursorMove(() => this.emit('cursormove')); this.onKey(e => this.emit('key', e.key, e.domEvent)); this.onResize(e => this.emit('resize', e)); this.onTitleChange(e => this.emit('title', e)); @@ -350,7 +350,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._userScrolling = false; this._inputHandler = new InputHandler(this); + this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); this.register(this._inputHandler); + // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; @@ -769,7 +771,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.viewport.onThemeChanged(this.renderer.colorManager.colors); this.register(this.viewport); - this.register(this.addDisposableListener('cursormove', () => this.renderer.onCursorMove())); + this.register(this.onCursorMove(() => this.renderer.onCursorMove())); this.register(this.onResize(() => this.renderer.onResize(this.cols, this.rows))); this.register(this.addDisposableListener('blur', () => this.renderer.onBlur())); this.register(this.addDisposableListener('focus', () => this.renderer.onFocus())); From f111aa27dc5a076402ee4d82153343c11c07b615 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 19:35:41 -0700 Subject: [PATCH 29/40] Convert linefeed to EventEmitter2 --- src/AccessibilityManager.ts | 2 +- src/InputHandler.ts | 10 ++++------ src/Terminal.test.ts | 12 +++++++++++- src/Terminal.ts | 4 +++- src/WindowsMode.ts | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index a8e0ba1b..866dc3c7 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -77,7 +77,7 @@ export class AccessibilityManager extends Disposable { this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows())); // Line feed is an issue as the prompt won't be read out after a command is run this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char))); - this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n'))); + this.register(this._terminal.onLineFeed(() => this._onChar('\n'))); this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount))); this.register(this._terminal.onKey(e => this._onKey(e.key))); this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion())); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index d0471286..da167e26 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -109,6 +109,8 @@ export class InputHandler extends Disposable implements IInputHandler { private _onCursorMove = new EventEmitter2(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } + private _onLineFeed = new EventEmitter2(); + public get onLineFeed(): IEvent { return this._onLineFeed.event; } constructor( protected _terminal: IInputHandlingTerminal, @@ -458,12 +460,8 @@ export class InputHandler extends Disposable implements IInputHandler { if (buffer.x >= this._terminal.cols) { buffer.x--; } - /** - * This event is emitted whenever the terminal outputs a LF or NL. - * - * @event linefeed - */ - this._terminal.emit('linefeed'); + + this._onLineFeed.fire(); } /** diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 2422c038..b44baa5b 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -17,7 +17,7 @@ class TestTerminal extends Terminal { public keyPress(ev: any): boolean { return this._keyPress(ev); } } -describe('term.js addons', () => { +describe('xterm.js', () => { let term: TestTerminal; const termOptions = { cols: INIT_COLS, @@ -95,6 +95,16 @@ describe('term.js addons', () => { }); }); + describe('linefeed', () => { + it('should emit a linefeed event', (done) => { + term.on('linefeed', () => { + done(); + }); + + term.write('\n'); + }); + }); + describe(`keypress (including 'key' event)`, () => { it('should receive a string and event object', (done) => { let steps = 0; diff --git a/src/Terminal.ts b/src/Terminal.ts index 64178c74..662ce7eb 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -261,7 +261,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._setup(); // TODO: Replace EventEmitter with EventEmitter2 internally - this.on('linefeed', () => this._onLineFeed.fire()); this.on('selection', () => this._onSelectionChange.fire()); this.on('data', e => this._onInput.fire(e)); this.on('scroll', e => this._onScroll.fire(e)); @@ -271,6 +270,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Fire old style events from new emitters this.onCursorMove(() => this.emit('cursormove')); this.onKey(e => this.emit('key', e.key, e.domEvent)); + this.onLineFeed(() => this.emit('linefeed')); this.onResize(e => this.emit('resize', e)); this.onTitleChange(e => this.emit('title', e)); } @@ -349,8 +349,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // this._writeStopped = false; this._userScrolling = false; + // Register input handler and refire/handle events this._inputHandler = new InputHandler(this); this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); + this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); this.register(this._inputHandler); // Reuse renderer if the Terminal is being recreated via a reset call. diff --git a/src/WindowsMode.ts b/src/WindowsMode.ts index 33a9bed5..ac1d193e 100644 --- a/src/WindowsMode.ts +++ b/src/WindowsMode.ts @@ -18,7 +18,7 @@ export function applyWindowsMode(terminal: ITerminal): IDisposable { // space. This is certainly not without its problems, but generally on // Windows when text reaches the end of the terminal it's likely going to be // wrapped. - return terminal.addDisposableListener('linefeed', () => { + return terminal.onLineFeed(() => { const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1); const lastChar = line.get(terminal.cols - 1); From 2c96c262f4a520b6e7c967d01a961670ccf2dc7d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 19:42:31 -0700 Subject: [PATCH 30/40] Convert scroll to EventEmitter2 --- src/AccessibilityManager.ts | 2 +- src/InputHandler.ts | 4 +++- src/Terminal.test.ts | 18 ++++++++++++++++-- src/Terminal.ts | 18 ++++++------------ 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index 866dc3c7..aeba5d8b 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -74,7 +74,7 @@ export class AccessibilityManager extends Disposable { this.register(this._renderRowsDebouncer); this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.rows))); this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end))); - this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows())); + this.register(this._terminal.onScroll(() => this._refreshRows())); // Line feed is an issue as the prompt won't be read out after a command is run this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char))); this.register(this._terminal.onLineFeed(() => this._onChar('\n'))); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index da167e26..c77b1c60 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -111,6 +111,8 @@ export class InputHandler extends Disposable implements IInputHandler { public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onLineFeed = new EventEmitter2(); public get onLineFeed(): IEvent { return this._onLineFeed.event; } + private _onScroll = new EventEmitter2(); + public get onScroll(): IEvent { return this._onScroll.event; } constructor( protected _terminal: IInputHandlingTerminal, @@ -764,7 +766,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0); this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0); // Force a scroll event to refresh viewport - this._terminal.emit('scroll', 0); + this._onScroll.fire(0); } break; } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index b44baa5b..b4818347 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -90,7 +90,6 @@ describe('xterm.js', () => { term.on('cursormove', () => { done(); }); - term.write('foo'); }); }); @@ -100,11 +99,26 @@ describe('xterm.js', () => { term.on('linefeed', () => { done(); }); - term.write('\n'); }); }); + describe('scroll', () => { + it('should emit a scroll event when scrollback is created', (done) => { + term.on('scroll', () => { + done(); + }); + term.write('\n'.repeat(INIT_ROWS)); + }); + it('should emit a scroll event when scrollback is cleared', (done) => { + term.write('\n'.repeat(INIT_ROWS)); + term.on('scroll', () => { + done(); + }); + term.clear(); + }); + }); + describe(`keypress (including 'key' event)`, () => { it('should receive a string and event object', (done) => { let steps = 0; diff --git a/src/Terminal.ts b/src/Terminal.ts index 662ce7eb..dfdb8569 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -263,7 +263,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: Replace EventEmitter with EventEmitter2 internally this.on('selection', () => this._onSelectionChange.fire()); this.on('data', e => this._onInput.fire(e)); - this.on('scroll', e => this._onScroll.fire(e)); this.on('refresh', e => this._onRender.fire(e)); // TODO: Remove these in v4 @@ -272,6 +271,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.onKey(e => this.emit('key', e.key, e.domEvent)); this.onLineFeed(() => this.emit('linefeed')); this.onResize(e => this.emit('resize', e)); + this.onScroll(e => this.emit('scroll', e)); this.onTitleChange(e => this.emit('title', e)); } @@ -740,7 +740,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._mouseZoneManager = new MouseZoneManager(this); this.register(this._mouseZoneManager); - this.register(this.addDisposableListener('scroll', () => this._mouseZoneManager.clearAll())); + this.register(this.onScroll(() => this._mouseZoneManager.clearAll())); this.linkifier.attachToDom(this._mouseZoneManager); this.textarea = document.createElement('textarea'); @@ -795,7 +795,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.textarea.focus(); this.textarea.select(); })); - this.register(this.addDisposableListener('scroll', () => { + this.register(this.onScroll(() => { this.viewport.syncScrollArea(); this.selectionManager.refresh(); })); @@ -1302,13 +1302,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.updateRange(this.buffer.scrollTop); this.updateRange(this.buffer.scrollBottom); - /** - * This event is emitted whenever the terminal is scrolled. - * The one parameter passed is the new y display position. - * - * @event scroll - */ - this.emit('scroll', this.buffer.ydisp); + this._onScroll.fire(this.buffer.ydisp); } /** @@ -1337,7 +1331,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } if (!suppressScrollEvent) { - this.emit('scroll', this.buffer.ydisp); + this._onScroll.fire(this.buffer.ydisp); } this.refresh(0, this.rows - 1); @@ -1825,7 +1819,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } this.refresh(0, this.rows - 1); - this.emit('scroll', this.buffer.ydisp); + this._onScroll.fire(this.buffer.ydisp); } /** From 87328fe29365a5b9fc2a64232c3e857eb4a01923 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 19:53:02 -0700 Subject: [PATCH 31/40] Convert data to EventEmitter2 --- src/InputHandler.ts | 8 +++++--- src/Terminal.ts | 28 ++++++++++++++-------------- src/Types.ts | 4 ---- src/addons/attach/Interfaces.ts | 1 + src/addons/attach/attach.ts | 6 ++++-- src/addons/terminado/Interfaces.ts | 7 ++++++- src/addons/terminado/terminado.ts | 7 ++++--- src/public/Terminal.ts | 2 +- src/ui/TestUtils.test.ts | 2 +- typings/xterm.d.ts | 4 ++-- 10 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index c77b1c60..fe8ab53d 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -109,6 +109,8 @@ export class InputHandler extends Disposable implements IInputHandler { private _onCursorMove = new EventEmitter2(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } + private _onData = new EventEmitter2(); + public get onData(): IEvent { return this._onData.event; } private _onLineFeed = new EventEmitter2(); public get onLineFeed(): IEvent { return this._onLineFeed.event; } private _onScroll = new EventEmitter2(); @@ -1725,13 +1727,13 @@ export class InputHandler extends Disposable implements IInputHandler { switch (params[0]) { case 5: // status report - this._terminal.emit('data', `${C0.ESC}[0n`); + this._onData.fire(`${C0.ESC}[0n`); break; case 6: // cursor position const y = this._terminal.buffer.y + 1; const x = this._terminal.buffer.x + 1; - this._terminal.emit('data', `${C0.ESC}[${y};${x}R`); + this._onData.fire(`${C0.ESC}[${y};${x}R`); break; } } else if (collect === '?') { @@ -1742,7 +1744,7 @@ export class InputHandler extends Disposable implements IInputHandler { // cursor position const y = this._terminal.buffer.y + 1; const x = this._terminal.buffer.x + 1; - this._terminal.emit('data', `${C0.ESC}[?${y};${x}R`); + this._onData.fire(`${C0.ESC}[?${y};${x}R`); break; case 15: // no printer diff --git a/src/Terminal.ts b/src/Terminal.ts index dfdb8569..a52da3d5 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -224,22 +224,22 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _onCursorMove = new EventEmitter2(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onLineFeed = new EventEmitter2(); - public get onLineFeed(): IEvent { return this._onLineFeed.event; } - private _onSelectionChange = new EventEmitter2(); - public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } - private _onInput = new EventEmitter2(); - public get onInput(): IEvent { return this._onInput.event; } - private _onTitleChange = new EventEmitter2(); - public get onTitleChange(): IEvent { return this._onTitleChange.event; } - private _onScroll = new EventEmitter2(); - public get onScroll(): IEvent { return this._onScroll.event; } + private _onData = new EventEmitter2(); + public get onData(): IEvent { return this._onData.event; } private _onKey = new EventEmitter2<{ key: string, domEvent: KeyboardEvent }>(); public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } + private _onLineFeed = new EventEmitter2(); + public get onLineFeed(): IEvent { return this._onLineFeed.event; } private _onRender = new EventEmitter2<{ start: number, end: number }>(); public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } private _onResize = new EventEmitter2<{ cols: number, rows: number }>(); public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } + private _onScroll = new EventEmitter2(); + public get onScroll(): IEvent { return this._onScroll.event; } + private _onSelectionChange = new EventEmitter2(); + public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } + private _onTitleChange = new EventEmitter2(); + public get onTitleChange(): IEvent { return this._onTitleChange.event; } /** * Creates a new `Terminal` object. @@ -262,12 +262,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: Replace EventEmitter with EventEmitter2 internally this.on('selection', () => this._onSelectionChange.fire()); - this.on('data', e => this._onInput.fire(e)); this.on('refresh', e => this._onRender.fire(e)); // TODO: Remove these in v4 // Fire old style events from new emitters this.onCursorMove(() => this.emit('cursormove')); + this.onData(e => this.emit('data', e)); this.onKey(e => this.emit('key', e.key, e.domEvent)); this.onLineFeed(() => this.emit('linefeed')); this.onResize(e => this.emit('resize', e)); @@ -313,7 +313,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.rows = Math.max(this.options.rows, MINIMUM_ROWS); if (this.options.handler) { - this.on('data', this.options.handler); + this.onData(this.options.handler); } this.cursorState = 0; @@ -1831,7 +1831,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } /** - * Emit the 'data' event and populate the given data. + * Emit the data event and populate the given data. * @param data The data to populate in the event. */ public handler(data: string): void { @@ -1849,7 +1849,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (this.buffer.ybase !== this.buffer.ydisp) { this.scrollToBottom(); } - this.emit('data', data); + this._onData.fire(data); } /** diff --git a/src/Types.ts b/src/Types.ts index 0fcf291e..ef99f147 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -221,10 +221,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce bracketedPasteMode: boolean; applicationCursor: boolean; - /** - * Emit the 'data' event and populate the given data. - * @param data The data to populate in the event. - */ handler(data: string): void; scrollLines(disp: number, suppressScrollEvent?: boolean): void; cancel(ev: Event, force?: boolean): boolean | void; diff --git a/src/addons/attach/Interfaces.ts b/src/addons/attach/Interfaces.ts index ab5846f5..4b269099 100644 --- a/src/addons/attach/Interfaces.ts +++ b/src/addons/attach/Interfaces.ts @@ -14,6 +14,7 @@ export interface IAttachAddonTerminal extends Terminal { __socket?: WebSocket; __attachSocketBuffer?: string; + __dataListener?: IDisposable; __getMessage?(ev: MessageEvent): void; __flushBuffer?(): void; diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts index f121e2e2..2c8a5d4d 100644 --- a/src/addons/attach/attach.ts +++ b/src/addons/attach/attach.ts @@ -90,7 +90,8 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage)); if (bidirectional) { - addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData)); + addonTerminal.__dataListener = addonTerminal.onData(addonTerminal.__sendData); + addonTerminal._core.register(addonTerminal.__dataListener); } addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket))); @@ -119,7 +120,8 @@ function addSocketListener(socket: WebSocket, type: string, handler: (this: WebS */ export function detach(term: Terminal, socket: WebSocket): void { const addonTerminal = term; - addonTerminal.off('data', addonTerminal.__sendData); + addonTerminal.__dataListener.dispose(); + addonTerminal.__dataListener = undefined; socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; diff --git a/src/addons/terminado/Interfaces.ts b/src/addons/terminado/Interfaces.ts index 8f17b0cc..dd7b045c 100644 --- a/src/addons/terminado/Interfaces.ts +++ b/src/addons/terminado/Interfaces.ts @@ -5,11 +5,16 @@ * Implements the attach method, that attaches the terminal to a WebSocket stream. */ -import { Terminal } from 'xterm'; +import { Terminal, IDisposable } from 'xterm'; export interface ITerminadoAddonTerminal extends Terminal { + _core: { + register(d: T): void; + }; + __socket?: WebSocket; __attachSocketBuffer?: string; + __dataListener?: IDisposable; __getMessage?(ev: MessageEvent): void; __flushBuffer?(): void; diff --git a/src/addons/terminado/terminado.ts b/src/addons/terminado/terminado.ts index b36c2cbf..cefa8087 100644 --- a/src/addons/terminado/terminado.ts +++ b/src/addons/terminado/terminado.ts @@ -59,9 +59,9 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional socket.addEventListener('message', addonTerminal.__getMessage); if (bidirectional) { - addonTerminal.onInput(addonTerminal.__sendData); + addonTerminal._core.register(addonTerminal.onData(addonTerminal.__sendData); } - addonTerminal.onResize(addonTerminal.__setSize); + addonTerminal._core.register(addonTerminal.onResize(addonTerminal.__setSize)); socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket)); socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket)); @@ -75,7 +75,8 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional */ export function terminadoDetach(term: Terminal, socket: WebSocket): void { const addonTerminal = term; - addonTerminal.off('data', addonTerminal.__sendData); + addonTerminal.__dataListener.dispose(); + addonTerminal.__dataListener = undefined; socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 53c3c693..d05a4f10 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -19,7 +19,7 @@ export class Terminal implements ITerminalApi { public get onCursorMove(): IEvent { return this._core.onCursorMove; } public get onLineFeed(): IEvent { return this._core.onLineFeed; } public get onSelectionChange(): IEvent { return this._core.onSelectionChange; } - public get onInput(): IEvent { return this._core.onInput; } + public get onData(): IEvent { return this._core.onData; } public get onTitleChange(): IEvent { return this._core.onTitleChange; } public get onScroll(): IEvent { return this._core.onScroll; } public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; } diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index ddf8e2c8..ae57d1f9 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -23,7 +23,7 @@ export class MockTerminal implements ITerminal { onCursorMove: IEvent; onLineFeed: IEvent; onSelectionChange: IEvent; - onInput: IEvent; + onData: IEvent; onTitleChange: IEvent; onScroll: IEvent; onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index f8bbdbdb..2c4bb2ec 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -392,13 +392,13 @@ declare module 'xterm' { onSelectionChange: IEvent; /** - * Adds an event listener for when an input event fires. This happens for + * Adds an event listener for when a data event fires. This happens for * example when the user types or pastes into the terminal. The event value * is whatever `string` results, in a typical setup, this should be passed * on to the backing pty. * @returns an `IDisposable` to stop listening. */ - onInput: IEvent; + onData: IEvent; /** * Adds an event listener for when an OSC 0 or OSC 2 title change occurs. From db78b75e8bd6a88b870daa5b1fa35a990afef476 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 19:55:33 -0700 Subject: [PATCH 32/40] Convert selection to EventEmitter2 --- src/SelectionManager.ts | 14 ++++++++------ src/Terminal.ts | 5 +++-- src/addons/terminado/terminado.ts | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 54499428..087e8995 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -108,8 +108,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _mouseDownTimeStamp: number; - private _onNewMouseSelection = new EventEmitter2(); - public get onNewMouseSelection(): IEvent { return this._onNewMouseSelection.event; } + private _onLinuxMouseSelection = new EventEmitter2(); + public get onLinuxMouseSelection(): IEvent { return this._onLinuxMouseSelection.event; } + private _onSelectionChange = new EventEmitter2(); + public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } constructor( private _terminal: ITerminal, @@ -262,7 +264,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager if (Browser.isLinux && isNewMouseSelection) { const selectionText = this.selectionText; if (selectionText.length) { - this._onNewMouseSelection.fire(this.selectionText); + this._onLinuxMouseSelection.fire(this.selectionText); } } } @@ -322,7 +324,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager public selectAll(): void { this._model.isSelectAllActive = true; this.refresh(); - this._terminal.emit('selection'); + this._onSelectionChange.fire(); } public selectLines(start: number, end: number): void { @@ -332,7 +334,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._model.selectionStart = [0, start]; this._model.selectionEnd = [this._terminal.cols, end]; this.refresh(); - this._terminal.emit('selection'); + this._onSelectionChange.fire(); } /** @@ -650,7 +652,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) { (new AltClickHandler(event, this._terminal)).move(); } else if (this.hasSelection) { - this._terminal.emit('selection'); + this._onSelectionChange.fire(); } } diff --git a/src/Terminal.ts b/src/Terminal.ts index a52da3d5..59f675f3 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -261,7 +261,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._setup(); // TODO: Replace EventEmitter with EventEmitter2 internally - this.on('selection', () => this._onSelectionChange.fire()); this.on('refresh', e => this._onRender.fire(e)); // TODO: Remove these in v4 @@ -271,6 +270,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.onKey(e => this.emit('key', e.key, e.domEvent)); this.onLineFeed(() => this.emit('linefeed')); this.onResize(e => this.emit('resize', e)); + this.onSelectionChange(() => this.emit('selection')); this.onScroll(e => this.emit('scroll', e)); this.onTitleChange(e => this.emit('title', e)); } @@ -785,9 +785,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.register(this.renderer.addDisposableListener('resize', (dimensions) => this.viewport.syncScrollArea())); this.selectionManager = new SelectionManager(this, this.charMeasure); + this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire())); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e))); this.register(this.selectionManager.addDisposableListener('refresh', data => this.renderer.onSelectionChanged(data.start, data.end, data.columnSelectMode))); - this.register(this.selectionManager.onNewMouseSelection(text => { + this.register(this.selectionManager.onLinuxMouseSelection(text => { // If there's a new selection, put it into the textarea, focus and select it // in order to register it as a selection on the OS. This event is fired // only on Linux to enable middle click to paste selection. diff --git a/src/addons/terminado/terminado.ts b/src/addons/terminado/terminado.ts index cefa8087..9895a07b 100644 --- a/src/addons/terminado/terminado.ts +++ b/src/addons/terminado/terminado.ts @@ -59,7 +59,7 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional socket.addEventListener('message', addonTerminal.__getMessage); if (bidirectional) { - addonTerminal._core.register(addonTerminal.onData(addonTerminal.__sendData); + addonTerminal._core.register(addonTerminal.onData(addonTerminal.__sendData)); } addonTerminal._core.register(addonTerminal.onResize(addonTerminal.__setSize)); From 290bde616331026e1a591b341b4da249b239d0f0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 20:16:26 -0700 Subject: [PATCH 33/40] Convert refresh to EventEmitter2 --- src/Terminal.ts | 9 ++++----- src/renderer/Renderer.ts | 6 +++++- src/renderer/Types.ts | 3 +++ src/renderer/dom/DomRenderer.ts | 6 +++++- src/ui/TestUtils.test.ts | 1 + 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 59f675f3..4a667387 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -260,15 +260,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.options = clone(options); this._setup(); - // TODO: Replace EventEmitter with EventEmitter2 internally - this.on('refresh', e => this._onRender.fire(e)); - // TODO: Remove these in v4 // Fire old style events from new emitters this.onCursorMove(() => this.emit('cursormove')); this.onData(e => this.emit('data', e)); this.onKey(e => this.emit('key', e.key, e.domEvent)); this.onLineFeed(() => this.emit('linefeed')); + this.onRender(e => this.emit('refresh', e)); this.onResize(e => this.emit('resize', e)); this.onSelectionChange(() => this.emit('selection')); this.onScroll(e => this.emit('scroll', e)); @@ -687,8 +685,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.register(addDisposableDomListener(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart())); this.register(addDisposableDomListener(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e))); this.register(addDisposableDomListener(this.textarea, 'compositionend', () => this._compositionHelper.compositionend())); - this.register(this.addDisposableListener('refresh', () => this._compositionHelper.updateCompositionElements())); - this.register(this.addDisposableListener('refresh', (data) => this._queueLinkification(data.start, data.end))); + this.register(this.onRender(() => this._compositionHelper.updateCompositionElements())); + this.register(this.onRender(e => this._queueLinkification(e.start, e.end))); } /** @@ -838,6 +836,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break; default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } + this.renderer.onRender(e => this._onRender.fire(e)); this.register(this.renderer); } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 2c1b516a..b7ad6854 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -15,6 +15,7 @@ import { RenderDebouncer } from '../ui/RenderDebouncer'; import { ScreenDprMonitor } from '../ui/ScreenDprMonitor'; import { ITheme } from 'xterm'; import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry'; +import { EventEmitter2, IEvent } from '../common/EventEmitter2'; export class Renderer extends EventEmitter implements IRenderer { private _renderDebouncer: RenderDebouncer; @@ -29,6 +30,9 @@ export class Renderer extends EventEmitter implements IRenderer { public colorManager: ColorManager; public dimensions: IRenderDimensions; + private _onRender = new EventEmitter2<{ start: number, end: number }>(); + public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } + constructor(private _terminal: ITerminal, theme: ITheme) { super(); const allowTransparency = this._terminal.options.allowTransparency; @@ -197,7 +201,7 @@ export class Renderer extends EventEmitter implements IRenderer { */ private _renderRows(start: number, end: number): void { this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); - this._terminal.emit('refresh', { start, end }); + this._onRender.fire({ start, end }); } /** diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index f2271f95..71e5fc30 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -5,6 +5,7 @@ import { ITerminal, CharacterJoinerHandler } from '../Types'; import { IEventEmitter, ITheme, IDisposable } from 'xterm'; +import { IEvent } from '../common/EventEmitter2'; /** * Flags used to render terminal text properly. @@ -27,6 +28,8 @@ export interface IRenderer extends IEventEmitter, IDisposable { dimensions: IRenderDimensions; colorManager: IColorManager; + onRender: IEvent<{ start: number, end: number }>; + dispose(): void; setTheme(theme: ITheme): IColorSet; onWindowResize(devicePixelRatio: number): void; diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 78ccc620..0f70e93b 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -11,6 +11,7 @@ import { ColorManager } from '../ColorManager'; import { RenderDebouncer } from '../../ui/RenderDebouncer'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; +import { EventEmitter2, IEvent } from '../../common/EventEmitter2'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -43,6 +44,9 @@ export class DomRenderer extends EventEmitter implements IRenderer { public dimensions: IRenderDimensions; public colorManager: ColorManager; + private _onRender = new EventEmitter2<{ start: number, end: number }>(); + public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } + constructor(private _terminal: ITerminal, theme: ITheme | undefined) { super(); const allowTransparency = this._terminal.options.allowTransparency; @@ -350,7 +354,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols)); } - this._terminal.emit('refresh', {start, end}); + this._onRender.fire({ start, end }); } private get _terminalSelector(): string { diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index ae57d1f9..9488a927 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -354,6 +354,7 @@ export class MockBuffer implements IBuffer { } export class MockRenderer implements IRenderer { + onRender: IEvent<{ start: number; end: number; }>; dispose(): void { throw new Error('Method not implemented.'); } From 2e0e24c382f623dc76759ce86a25395ec106769e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 20:26:18 -0700 Subject: [PATCH 34/40] Remove extends EventEmitter from SelectinoManager --- src/AccessibilityManager.ts | 4 ++-- src/SelectionManager.ts | 21 ++++++++++----------- src/Terminal.ts | 2 +- src/Types.ts | 6 ++++++ 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index aeba5d8b..d72f6647 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -72,8 +72,8 @@ export class AccessibilityManager extends Disposable { this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot); this.register(this._renderRowsDebouncer); - this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.rows))); - this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end))); + this.register(this._terminal.onResize(e => this._onResize(e.rows))); + this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end))); this.register(this._terminal.onScroll(() => this._refreshRows())); // Line feed is an issue as the prompt won't be read out after a command is run this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char))); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 087e8995..eed206cc 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,11 +3,10 @@ * @license MIT */ -import { ITerminal, ISelectionManager, IBuffer, IBufferLine } from './Types'; +import { ITerminal, ISelectionManager, IBuffer, IBufferLine, ISelectionRedrawRequestEvent } from './Types'; import { MouseHelper } from './ui/MouseHelper'; import * as Browser from './common/Platform'; import { CharMeasure } from './ui/CharMeasure'; -import { EventEmitter } from './common/EventEmitter'; import { SelectionModel } from './SelectionModel'; import { AltClickHandler } from './handlers/AltClickHandler'; import { CellData } from './BufferLine'; @@ -68,10 +67,10 @@ export const enum SelectionMode { * SelectionModel, SelectionManager handles with all logic associated with * dealing with the selection, including handling mouse interaction, wide * characters and fetching the actual text within the selection. Rendering is - * not handled by the SelectionManager but a 'refresh' event is fired when the - * selection is ready to be redrawn. + * not handled by the SelectionManager but the onRedrawRequest event is fired + * when the selection is ready to be redrawn (on an animation frame). */ -export class SelectionManager extends EventEmitter implements ISelectionManager { +export class SelectionManager implements ISelectionManager { protected _model: SelectionModel; /** @@ -110,6 +109,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _onLinuxMouseSelection = new EventEmitter2(); public get onLinuxMouseSelection(): IEvent { return this._onLinuxMouseSelection.event; } + private _onRedrawRequest = new EventEmitter2(); + public get onRedrawRequest(): IEvent { return this._onRedrawRequest.event; } private _onSelectionChange = new EventEmitter2(); public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } @@ -117,7 +118,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _terminal: ITerminal, private _charMeasure: CharMeasure ) { - super(); this._initListeners(); this.enable(); @@ -126,7 +126,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } public dispose(): void { - super.dispose(); this._removeMouseDownListeners(); } @@ -250,10 +249,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager /** * Queues a refresh, redrawing the selection on the next opportunity. - * @param isNewMouseSelection Whether the selection should be registered as a new + * @param isLinuxMouseSelection Whether the selection should be registered as a new * selection on Linux. */ - public refresh(isNewMouseSelection?: boolean): void { + public refresh(isLinuxMouseSelection?: boolean): void { // Queue the refresh for the renderer if (!this._refreshAnimationFrame) { this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh()); @@ -261,7 +260,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // If the platform is Linux and the refresh call comes from a mouse event, // we need to update the selection for middle click to paste selection. - if (Browser.isLinux && isNewMouseSelection) { + if (Browser.isLinux && isLinuxMouseSelection) { const selectionText = this.selectionText; if (selectionText.length) { this._onLinuxMouseSelection.fire(this.selectionText); @@ -275,7 +274,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager */ private _refresh(): void { this._refreshAnimationFrame = null; - this.emit('refresh', { + this._onRedrawRequest.fire({ start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd, columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN diff --git a/src/Terminal.ts b/src/Terminal.ts index 4a667387..cb2743fd 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -785,7 +785,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.selectionManager = new SelectionManager(this, this.charMeasure); this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire())); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e))); - this.register(this.selectionManager.addDisposableListener('refresh', data => this.renderer.onSelectionChanged(data.start, data.end, data.columnSelectMode))); + this.register(this.selectionManager.onRedrawRequest(e => this.renderer.onSelectionChanged(e.start, e.end, e.columnSelectMode))); this.register(this.selectionManager.onLinuxMouseSelection(text => { // If there's a new selection, put it into the textarea, focus and select it // in order to register it as a selection on the OS. This event is fired diff --git a/src/Types.ts b/src/Types.ts index ef99f147..be75b0c9 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -324,6 +324,12 @@ export interface ISelectionManager { selectWordAtCursor(event: MouseEvent): void; } +export interface ISelectionRedrawRequestEvent { + start: [number, number]; + end: [number, number]; + columnSelectMode: boolean; +} + export interface ILinkifier extends IEventEmitter { attachToDom(mouseZoneManager: IMouseZoneManager): void; linkifyRows(start: number, end: number): void; From 36567e60df8220374b58d538b63a582cb8279502 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 20:36:08 -0700 Subject: [PATCH 35/40] Remove extends EventEmitter from more classes --- src/AccessibilityManager.ts | 2 +- src/Buffer.ts | 5 ++--- src/Terminal.ts | 2 +- src/renderer/Renderer.ts | 8 +++++--- src/renderer/Types.ts | 5 +++-- src/renderer/dom/DomRenderer.ts | 10 ++++++++-- src/ui/TestUtils.test.ts | 1 + 7 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index d72f6647..e2115797 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -85,7 +85,7 @@ export class AccessibilityManager extends Disposable { // should be listened to instead? That would mean that the order of events are always // guarenteed this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions())); - this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions())); + this.register(this._terminal.renderer.onCanvasResize(() => this._refreshRowsDimensions())); // This shouldn't be needed on modern browsers but is present in case the // media query that drives the dprchange event isn't supported this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions())); diff --git a/src/Buffer.ts b/src/Buffer.ts index ba686238..c1c08c85 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -5,12 +5,12 @@ import { CircularList, IInsertEvent } from './common/CircularList'; import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData, IAttributeData } from './Types'; -import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine, CellData, AttributeData } from './BufferLine'; import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from './BufferReflow'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; import { EventEmitter2, IEvent } from './common/EventEmitter2'; +import { Disposable } from '../lib/common/Lifecycle'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); @@ -629,7 +629,7 @@ export class Buffer implements IBuffer { } } -export class Marker extends EventEmitter implements IMarker { +export class Marker extends Disposable implements IMarker { private static _nextId = 1; private _id: number = Marker._nextId++; @@ -653,7 +653,6 @@ export class Marker extends EventEmitter implements IMarker { this.isDisposed = true; // Emit before super.dispose such that dispose listeners get a change to react this._onDispose.fire(); - super.dispose(); } } diff --git a/src/Terminal.ts b/src/Terminal.ts index cb2743fd..16ac69b3 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -780,7 +780,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // matchMedia query. this.register(addDisposableDomListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio))); this.register(this.charMeasure.onCharSizeChanged(() => this.renderer.onCharSizeChanged())); - this.register(this.renderer.addDisposableListener('resize', (dimensions) => this.viewport.syncScrollArea())); + this.register(this.renderer.onCanvasResize(() => this.viewport.syncScrollArea())); this.selectionManager = new SelectionManager(this, this.charMeasure); this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire())); diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index b7ad6854..2a205be3 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -10,14 +10,14 @@ import { ColorManager } from './ColorManager'; import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; import { ITerminal, CharacterJoinerHandler } from '../Types'; import { LinkRenderLayer } from './LinkRenderLayer'; -import { EventEmitter } from '../common/EventEmitter'; import { RenderDebouncer } from '../ui/RenderDebouncer'; import { ScreenDprMonitor } from '../ui/ScreenDprMonitor'; import { ITheme } from 'xterm'; import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry'; import { EventEmitter2, IEvent } from '../common/EventEmitter2'; +import { Disposable } from '../common/Lifecycle'; -export class Renderer extends EventEmitter implements IRenderer { +export class Renderer extends Disposable implements IRenderer { private _renderDebouncer: RenderDebouncer; private _renderLayers: IRenderLayer[]; @@ -30,6 +30,8 @@ export class Renderer extends EventEmitter implements IRenderer { public colorManager: ColorManager; public dimensions: IRenderDimensions; + private _onCanvasResize = new EventEmitter2<{ width: number, height: number }>(); + public get onCanvasResize(): IEvent<{ width: number, height: number }> { return this._onCanvasResize.event; } private _onRender = new EventEmitter2<{ start: number, end: number }>(); public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } @@ -138,7 +140,7 @@ export class Renderer extends EventEmitter implements IRenderer { this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; - this.emit('resize', { + this._onCanvasResize.fire({ width: this.dimensions.canvasWidth, height: this.dimensions.canvasHeight }); diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 71e5fc30..9d2ddcc3 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -4,7 +4,7 @@ */ import { ITerminal, CharacterJoinerHandler } from '../Types'; -import { IEventEmitter, ITheme, IDisposable } from 'xterm'; +import { ITheme, IDisposable } from 'xterm'; import { IEvent } from '../common/EventEmitter2'; /** @@ -24,10 +24,11 @@ export const enum FLAGS { * Note that IRenderer implementations should emit the refresh event after * rendering rows to the screen. */ -export interface IRenderer extends IEventEmitter, IDisposable { +export interface IRenderer extends IDisposable { dimensions: IRenderDimensions; colorManager: IColorManager; + onCanvasResize: IEvent<{ width: number, height: number }>; onRender: IEvent<{ start: number, end: number }>; dispose(): void; diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 0f70e93b..c13b6e27 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -6,12 +6,12 @@ import { IRenderer, IRenderDimensions, IColorSet } from '../Types'; import { ILinkHoverEvent, ITerminal, CharacterJoinerHandler, LinkHoverEventTypes } from '../../Types'; import { ITheme } from 'xterm'; -import { EventEmitter } from '../../common/EventEmitter'; import { ColorManager } from '../ColorManager'; import { RenderDebouncer } from '../../ui/RenderDebouncer'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; import { EventEmitter2, IEvent } from '../../common/EventEmitter2'; +import { Disposable } from '../../common/Lifecycle'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -30,7 +30,7 @@ let nextTerminalId = 1; * particularly fast or feature complete, more just stable and usable for when * canvas is not an option. */ -export class DomRenderer extends EventEmitter implements IRenderer { +export class DomRenderer extends Disposable implements IRenderer { private _renderDebouncer: RenderDebouncer; private _rowFactory: DomRendererRowFactory; private _terminalClass: number = nextTerminalId++; @@ -44,6 +44,8 @@ export class DomRenderer extends EventEmitter implements IRenderer { public dimensions: IRenderDimensions; public colorManager: ColorManager; + private _onCanvasResize = new EventEmitter2<{ width: number, height: number }>(); + public get onCanvasResize(): IEvent<{ width: number, height: number }> { return this._onCanvasResize.event; } private _onRender = new EventEmitter2<{ start: number, end: number }>(); public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } @@ -242,6 +244,10 @@ export class DomRenderer extends EventEmitter implements IRenderer { public onResize(cols: number, rows: number): void { this._refreshRowElements(cols, rows); this._updateDimensions(); + this._onCanvasResize.fire({ + width: this.dimensions.canvasWidth, + height: this.dimensions.canvasHeight + }); } public onCharSizeChanged(): void { diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 9488a927..5ee0f9d5 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -354,6 +354,7 @@ export class MockBuffer implements IBuffer { } export class MockRenderer implements IRenderer { + onCanvasResize: IEvent<{ width: number; height: number; }>; onRender: IEvent<{ start: number; end: number; }>; dispose(): void { throw new Error('Method not implemented.'); From f4c7e3fd7dbeede76668eabe212bf261afc67a79 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 20:43:42 -0700 Subject: [PATCH 36/40] Remove extends EventEmitter from Linkifier --- src/Linkifier.ts | 24 +++++++++++++++--------- src/Types.ts | 14 ++++++-------- src/renderer/LinkRenderLayer.ts | 12 ++++++------ src/renderer/dom/DomRenderer.ts | 10 +++++----- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 80399904..8c57e1b2 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -4,15 +4,15 @@ */ import { IMouseZoneManager } from './ui/Types'; -import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types'; +import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types'; import { MouseZone } from './ui/MouseZoneManager'; -import { EventEmitter } from './common/EventEmitter'; import { getStringCellWidth } from './CharWidth'; +import { EventEmitter2, IEvent } from './common/EventEmitter2'; /** * The Linkifier applies links to rows shortly after they have been refreshed. */ -export class Linkifier extends EventEmitter implements ILinkifier { +export class Linkifier implements ILinkifier { /** * The time to wait after a row is changed before it is linkified. This prevents * the costly operation of searching every row multiple times, potentially a @@ -34,10 +34,16 @@ export class Linkifier extends EventEmitter implements ILinkifier { private _nextLinkMatcherId = 0; private _rowsToLinkify: { start: number, end: number }; + private _onLinkHover = new EventEmitter2(); + public get onLinkHover(): IEvent { return this._onLinkHover.event; } + private _onLinkLeave = new EventEmitter2(); + public get onLinkLeave(): IEvent { return this._onLinkLeave.event; } + private _onLinkTooltip = new EventEmitter2(); + public get onLinkTooltip(): IEvent { return this._onLinkTooltip.event; } + constructor( protected _terminal: ITerminal ) { - super(); this._rowsToLinkify = { start: null, end: null @@ -283,18 +289,18 @@ export class Linkifier extends EventEmitter implements ILinkifier { } window.open(uri, '_blank'); }, - e => { - this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2, fg)); + () => { + this._onLinkHover.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); this._terminal.element.classList.add('xterm-cursor-pointer'); }, e => { - this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg)); + this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); if (matcher.hoverTooltipCallback) { matcher.hoverTooltipCallback(e, uri); } }, () => { - this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg)); + this._onLinkLeave.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); this._terminal.element.classList.remove('xterm-cursor-pointer'); if (matcher.hoverLeaveCallback) { matcher.hoverLeaveCallback(); @@ -309,7 +315,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { )); } - private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent { + private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkifierEvent { return { x1, y1, x2, y2, cols: this._terminal.cols, fg }; } } diff --git a/src/Types.ts b/src/Types.ts index be75b0c9..a70dfdad 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -23,12 +23,6 @@ export type CharacterJoinerHandler = (text: string) => [number, number][]; // BufferIndex denotes a position in the buffer: [rowIndex, colIndex] export type BufferIndex = [number, number]; -export const enum LinkHoverEventTypes { - HOVER = 'linkhover', - TOOLTIP = 'linktooltip', - LEAVE = 'linkleave' -} - /** * This interface encapsulates everything needed from the Terminal by the * InputHandler. This cleanly separates the large amount of methods needed by @@ -194,7 +188,7 @@ export interface ILinkMatcher { willLinkActivate?: (event: MouseEvent, uri: string) => boolean; } -export interface ILinkHoverEvent { +export interface ILinkifierEvent { x1: number; y1: number; x2: number; @@ -330,7 +324,11 @@ export interface ISelectionRedrawRequestEvent { columnSelectMode: boolean; } -export interface ILinkifier extends IEventEmitter { +export interface ILinkifier { + onLinkHover: IEvent; + onLinkLeave: IEvent; + onLinkTooltip: IEvent; + attachToDom(mouseZoneManager: IMouseZoneManager): void; linkifyRows(start: number, end: number): void; registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 855830e4..abb899b0 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -3,19 +3,19 @@ * @license MIT */ -import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types'; +import { ILinkifierEvent, ITerminal, ILinkifierAccessor } from '../Types'; import { IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; import { is256Color } from './atlas/CharAtlasUtils'; export class LinkRenderLayer extends BaseRenderLayer { - private _state: ILinkHoverEvent = null; + private _state: ILinkifierEvent = null; constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) { super(container, 'link', zIndex, true, colors); - terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e)); - terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e)); + terminal.linkifier.onLinkHover(e => this._onLinkHover(e)); + terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e)); } public resize(terminal: ITerminal, dim: IRenderDimensions): void { @@ -40,7 +40,7 @@ export class LinkRenderLayer extends BaseRenderLayer { } } - private _onLinkHover(e: ILinkHoverEvent): void { + private _onLinkHover(e: ILinkifierEvent): void { if (e.fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.background.css; } else if (is256Color(e.fg)) { @@ -64,7 +64,7 @@ export class LinkRenderLayer extends BaseRenderLayer { this._state = e; } - private _onLinkLeave(e: ILinkHoverEvent): void { + private _onLinkLeave(e: ILinkifierEvent): void { this._clearCurrentLink(); } } diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index c13b6e27..1d879fcd 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -4,7 +4,7 @@ */ import { IRenderer, IRenderDimensions, IColorSet } from '../Types'; -import { ILinkHoverEvent, ITerminal, CharacterJoinerHandler, LinkHoverEventTypes } from '../../Types'; +import { ILinkifierEvent, ITerminal, CharacterJoinerHandler } from '../../Types'; import { ITheme } from 'xterm'; import { ColorManager } from '../ColorManager'; import { RenderDebouncer } from '../../ui/RenderDebouncer'; @@ -87,8 +87,8 @@ export class DomRenderer extends Disposable implements IRenderer { this._terminal.screenElement.appendChild(this._rowContainer); this._terminal.screenElement.appendChild(this._selectionContainer); - this._terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e)); - this._terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e)); + this._terminal.linkifier.onLinkHover(e => this._onLinkHover(e)); + this._terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e)); } public dispose(): void { @@ -370,11 +370,11 @@ export class DomRenderer extends Disposable implements IRenderer { public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; } public deregisterCharacterJoiner(joinerId: number): boolean { return false; } - private _onLinkHover(e: ILinkHoverEvent): void { + private _onLinkHover(e: ILinkifierEvent): void { this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true); } - private _onLinkLeave(e: ILinkHoverEvent): void { + private _onLinkLeave(e: ILinkifierEvent): void { this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false); } From b9bc219eccb1201acd76d0c3cb8b037a7245e133 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 20:52:18 -0700 Subject: [PATCH 37/40] Make events in d.ts alphabetical --- typings/xterm.d.ts | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2c4bb2ec..24a30b25 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -379,18 +379,6 @@ declare module 'xterm' { */ onCursorMove: IEvent; - /** - * Adds an event listener for when a line feed is added. - * @returns an `IDisposable` to stop listening. - */ - onLineFeed: IEvent; - - /** - * Adds an event listener for when a selection change occurs. - * @returns an `IDisposable` to stop listening. - */ - onSelectionChange: IEvent; - /** * Adds an event listener for when a data event fires. This happens for * example when the user types or pastes into the terminal. The event value @@ -401,11 +389,18 @@ declare module 'xterm' { onData: IEvent; /** - * Adds an event listener for when an OSC 0 or OSC 2 title change occurs. - * The event value is the new title. + * Adds an event listener for a key is pressed. The event value contains the + * string that will be sent in the data event as well as the DOM event that + * triggered it. * @returns an `IDisposable` to stop listening. */ - onTitleChange: IEvent; + onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; + + /** + * Adds an event listener for when a line feed is added. + * @returns an `IDisposable` to stop listening. + */ + onLineFeed: IEvent; /** * Adds an event listener for when a scroll occurs. The event value is the @@ -415,12 +410,10 @@ declare module 'xterm' { onScroll: IEvent; /** - * Adds an event listener for a key is pressed. The event value contains the - * string that will be sent in the data event as well as the DOM event that - * triggered it. + * Adds an event listener for when a selection change occurs. * @returns an `IDisposable` to stop listening. */ - onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; + onSelectionChange: IEvent; /** * Adds an event listener for when rows are rendered. The event value @@ -437,6 +430,13 @@ declare module 'xterm' { */ onResize: IEvent<{ cols: number, rows: number }>; + /** + * Adds an event listener for when an OSC 0 or OSC 2 title change occurs. + * The event value is the new title. + * @returns an `IDisposable` to stop listening. + */ + onTitleChange: IEvent; + /** * Unfocus the terminal. */ From cf4b59567bbd19852155a7ebee5188dd30763537 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 21:01:55 -0700 Subject: [PATCH 38/40] Add tests for new events --- src/Terminal.test.ts | 75 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index b4818347..dd8e32e1 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -68,6 +68,81 @@ describe('xterm.js', () => { }); }); + describe('events', () => { + it('should fire the onData evnet', (done) => { + term.onData(() => done()); + term.handler('fake'); + }); + it('should fire the onCursorMove event', (done) => { + term.on('cursormove', () => done()); + term.write('foo'); + }); + it('should fire the onLineFeed event', (done) => { + term.on('linefeed', () => done()); + term.write('\n'); + }); + it('should fire a scroll event when scrollback is created', (done) => { + term.on('scroll', () => done()); + term.write('\n'.repeat(INIT_ROWS)); + }); + it('should fire a scroll event when scrollback is cleared', (done) => { + term.write('\n'.repeat(INIT_ROWS)); + term.on('scroll', () => done()); + term.clear(); + }); + it('should fire a key event after a keypress DOM event', (done) => { + term.onKey(e => { + assert.equal(typeof e.key, 'string'); + expect(e.domEvent).to.be.an.instanceof(Object); + done(); + }); + const evKeyPress = { + preventDefault: () => { }, + stopPropagation: () => { }, + type: 'keypress', + keyCode: 13 + }; + term.keyPress(evKeyPress); + }); + it('should fire a key event after a keydown DOM event', (done) => { + term.onKey(e => { + assert.equal(typeof e.key, 'string'); + expect(e.domEvent).to.be.an.instanceof(Object); + done(); + }); + const evKeyDown = { + preventDefault: () => { }, + stopPropagation: () => { }, + type: 'keydown', + keyCode: 13 + }; + term.keyDown(evKeyDown); + }); + it('should fire the onResize event', (done) => { + term.onResize(e => { + expect(e).to.have.keys(['cols', 'rows']); + assert.equal(typeof e.cols, 'number'); + assert.equal(typeof e.rows, 'number'); + done(); + }); + term.resize(1, 1); + }); + it('should fire the onScroll event', (done) => { + term.onScroll(e => { + assert.equal(typeof e, 'number'); + done(); + }); + term.scroll(); + }); + it('should fire the onTitleChange event', (done) => { + term.onTitleChange(e => { + assert.equal(e, 'title'); + done(); + }); + term.handleTitle('title'); + }); + }); + describe('on', () => { beforeEach(() => { term.on('key', () => { }); From a0ee09a7bc1f664ae9e45f6961081a91c26e1812 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 11 Apr 2019 08:14:18 -0700 Subject: [PATCH 39/40] Add vscode setting for single quote style This will alwayts use single quotes when TS server does auto imports, implement interface, etc. --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..3c8a2dc2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.preferences.quoteStyle": "single" +} From 8718d3a4ca5d3db507438bcc13659fa22e5361c9 Mon Sep 17 00:00:00 2001 From: Nick Mitchell Date: Sat, 13 Apr 2019 21:12:59 -0400 Subject: [PATCH 40/40] feat: add underline support to DOM renderer part of #1896 --- src/renderer/dom/DomRendererRowFactory.test.ts | 10 ++++++++++ src/renderer/dom/DomRendererRowFactory.ts | 5 +++++ src/xterm.css | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 4402943c..076f5d6a 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -100,6 +100,16 @@ describe('DomRendererRowFactory', () => { ); }); + it('should add class for underline', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + it('should add classes for 256 foreground colors', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg |= Attributes.CM_P256; diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index a6cbeb78..60e2d509 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -11,6 +11,7 @@ import { CellData, AttributeData } from '../../BufferLine'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; export const ITALIC_CLASS = 'xterm-italic'; +export const UNDERLINE_CLASS = 'xterm-underline'; export const CURSOR_CLASS = 'xterm-cursor'; export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink'; export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block'; @@ -88,6 +89,10 @@ export class DomRendererRowFactory { charElement.classList.add(DIM_CLASS); } + if (this._workCell.isUnderline()) { + charElement.classList.add(UNDERLINE_CLASS); + } + charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; const swapColor = this._workCell.isInverse(); diff --git a/src/xterm.css b/src/xterm.css index e80c2524..5448b5a5 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -165,3 +165,7 @@ .xterm-dim { opacity: 0.5; } + +.xterm-underline { + text-decoration: underline; +}