From 565cba9c1ae6393c482b64e9a7a99bfa06e7776e Mon Sep 17 00:00:00 2001 From: ntchjb Date: Sat, 29 Dec 2018 09:38:17 +0700 Subject: [PATCH 1/9] 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 96cd845d40..3537295d17 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 2/9] 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 3537295d17..7200539bec 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 3/9] 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 7200539bec..69fd919f3b 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 4/9] 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 69fd919f3b..7200539bec 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 unwarpped row - result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); + // Search startRow + result = this._findInLine(term, startRow, startCol, 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 5/9] 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 7200539bec..0b7e896030 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 6/9] _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 0b7e896030..525e25dc7f 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 03e648a0fbc250438d28001036261bba951292ef Mon Sep 17 00:00:00 2001 From: ntchjb Date: Fri, 15 Mar 2019 19:15:41 +0700 Subject: [PATCH 7/9] 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 525e25dc7f..a2258e4811 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 8/9] 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 a2258e4811..57a8c0a888 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 8b5b64605e0c0168fb0c1b529eee52b0a9d74fea Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 9 Apr 2019 19:53:22 -0400 Subject: [PATCH 9/9] 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 57a8c0a888..3dfc889116 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