Skip to content

Commit

Permalink
fix: translate string offset to buffer index in wrapped line
Browse files Browse the repository at this point in the history
  • Loading branch information
gera2ld committed Feb 23, 2021
1 parent ae037bd commit e340005
Showing 1 changed file with 56 additions and 40 deletions.
96 changes: 56 additions & 40 deletions addons/xterm-addon-search/src/SearchAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class SearchAddon implements ITerminalAddon {
* We memoize the calls into an array that has a time based ttl.
* _linesCache is also invalidated when the terminal cursor moves.
*/
private _linesCache: string[] | undefined;
private _linesCache: [string, number[]][] | undefined;
private _linesCacheTimeoutId = 0;
private _cursorMoveListener: IDisposable | undefined;
private _resizeListener: IDisposable | undefined;
Expand Down Expand Up @@ -258,7 +258,7 @@ export class SearchAddon implements ITerminalAddon {
*/
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
const terminal = this._terminal!;
let row = searchPosition.startRow;
const row = searchPosition.startRow;
const col = searchPosition.startCol;

// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
Expand All @@ -275,13 +275,14 @@ export class SearchAddon implements ITerminalAddon {
searchPosition.startCol += terminal.cols;
return this._findInLine(term, searchPosition, searchOptions);
}
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
if (stringLine === void 0) {
stringLine = this._translateBufferLineToStringWithWrap(row, true);
let cache = this._linesCache?.[row];
if (!cache) {
cache = this._translateBufferLineToStringWithWrap(row, true);
if (this._linesCache) {
this._linesCache[row] = stringLine;
this._linesCache[row] = cache;
}
}
const [stringLine, offsets] = cache;

const offset = this._bufferColsToStringOffset(row, col);
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
Expand Down Expand Up @@ -316,51 +317,57 @@ export class SearchAddon implements ITerminalAddon {
}

if (resultIndex >= 0) {
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
if (resultIndex >= terminal.cols) {
row += Math.floor(resultIndex / terminal.cols);
resultIndex = resultIndex % terminal.cols;
}
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
return;
}

const line = terminal.buffer.active.getLine(row);
let size = term.length;

let col = 0;
if (line) {
col = this._stringLengthToBufferSize(line, resultIndex);
size = this._stringLengthToBufferSize(line, term.length, col);
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
let startRowOffset = 0;
while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {
startRowOffset++;
}
let endRowOffset = startRowOffset;
while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {
endRowOffset++;
}
const startColOffset = resultIndex - offsets[startRowOffset];
const endColOffset = resultIndex + term.length - offsets[endRowOffset];
const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);
const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);
const size = endColIndex - startColIndex + terminal.cols * (endRowOffset - startRowOffset);

return {
term,
col,
row,
col: startColIndex,
row: row + startRowOffset,
size
};
}
}

private _stringLengthToBufferSize(line: IBufferLine, length: number, start: number = 0): number {
for (let i = 0; i < length; i++) {
const cell = line.getCell(start + i);
private _stringLengthToBufferSize(row: number, offset: number): number {
const line = this._terminal!.buffer.active.getLine(row);
if (!line) {
return 0;
}
for (let i = 0; i < offset; i++) {
const cell = line.getCell(i);
if (!cell) {
break;
}
// Adjust the searchIndex to normalize emoji into single chars
const char = cell.getChars();
if (char.length > 1) {
length -= char.length - 1;
offset -= char.length - 1;
}
// Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK)
const nextCell = line.getCell(start + i + 1);
const nextCell = line.getCell(i + 1);
if (nextCell && nextCell.getWidth() === 0) {
length++;
offset++;
}
}
return length;
return offset;
}

private _bufferColsToStringOffset(startRow: number, cols: number): number {
Expand Down Expand Up @@ -396,23 +403,32 @@ export class SearchAddon implements ITerminalAddon {
* @param line The line being translated.
* @param trimRight Whether to trim whitespace to the right.
*/
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): [string, number[]] {
const terminal = this._terminal!;
let lineString = '';
let lineWrapsToNext: boolean;

do {
const strings = [];
const offsets = [0];
let line = terminal.buffer.active.getLine(lineIndex)!;
while (line) {
const nextLine = terminal.buffer.active.getLine(lineIndex + 1);
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
const line = terminal.buffer.active.getLine(lineIndex);
if (!line) {
break;
const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
let string = line.translateToString(!lineWrapsToNext && trimRight);
// Assume early wrapping, when the last cell of current line is empty and the first char on
// the next line is a wide char. This leads to a faulty handling if the empty cell was indeed
// meant to be empty.
if (lineWrapsToNext && line.getCell(line.length - 1)?.getCode() === 0 && (nextLine!.getCell(0)?.getWidth() || 0) > 1) {
string = string.slice(0, -1);
}
strings.push(string);
if (lineWrapsToNext) {
offsets.push(offsets[offsets.length - 1] + string.length);
}
lineString += line.translateToString(!lineWrapsToNext && trimRight).substring(0, terminal.cols);
lineIndex++;
} while (lineWrapsToNext);

return lineString;
if (!lineWrapsToNext) {
break;
}
line = nextLine!;
}
return [strings.join(''), offsets];
}

/**
Expand Down

0 comments on commit e340005

Please sign in to comment.