Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions addons/addon-search/src/SearchEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,50 @@ describe('SearchEngine', () => {

assert.strictEqual(searchEngine.find('world', 0, 0, { wholeWord: true }), undefined);
});

it('should find a whole word after a partial match on the same line', async () => {
await writeP(terminal, 'foobar foo');

assert.deepStrictEqual(searchEngine.find('foo', 0, 0, { wholeWord: true }), {
term: 'foo',
col: 7,
row: 0,
size: 3
});
});

it('should find a whole word after a partial match on the same line with regex', async () => {
await writeP(terminal, 'foobar foo');

assert.deepStrictEqual(searchEngine.find('fo+', 0, 0, { wholeWord: true, regex: true }), {
term: 'foo',
col: 7,
row: 0,
size: 3
});
});

it('should find a whole word before a partial match when searching backwards', async () => {
await writeP(terminal, 'foo barfoo');

assert.deepStrictEqual(searchEngine.findPreviousWithSelection('foo', { wholeWord: true }), {
term: 'foo',
col: 0,
row: 0,
size: 3
});
});

it('should find a whole word before a partial match when searching backwards with regex', async () => {
await writeP(terminal, 'foo barfoo');

assert.deepStrictEqual(searchEngine.findPreviousWithSelection('fo+', { wholeWord: true, regex: true }), {
term: 'foo',
col: 0,
row: 0,
size: 3
});
});
});

describe('regex', () => {
Expand Down Expand Up @@ -488,6 +532,39 @@ describe('SearchEngine', () => {
});
});

it('should find a whole word after a partial match within a wrapped line', async () => {
await writeP(terminal, 'foobar' + 'A'.repeat(100) + ' foo');

assert.deepStrictEqual(searchEngine.find('foo', 0, 0, { wholeWord: true }), {
term: 'foo',
col: 27,
row: 1,
size: 3
});
});

it('should find a match when starting from a wrapped continuation row', async () => {
await writeP(terminal, 'target' + 'A'.repeat(100) + 'target');

assert.deepStrictEqual(searchEngine.find('target', 1, 0), {
term: 'target',
col: 26,
row: 1,
size: 6
});
});

it('should find a match on the next line after a long wrapped line without a match', async () => {
await writeP(terminal, 'A'.repeat(80 * 50) + '\r\ntarget');

assert.deepStrictEqual(searchEngine.find('target', 0, 0), {
term: 'target',
col: 0,
row: 50,
size: 6
});
});

it('should skip wrapped lines correctly in findInLine', async () => {
const longText = 'A'.repeat(200);
await writeP(terminal, longText + '\r\nNext line with target');
Expand Down
56 changes: 45 additions & 11 deletions addons/addon-search/src/SearchEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export class SearchEngine {
// Search from startRow + 1 to end
if (!result) {
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
if (this._isWrappedRow(y)) {
continue;
}
searchPosition.startRow = y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
Expand Down Expand Up @@ -127,6 +130,9 @@ export class SearchEngine {
// Search from startRow + 1 to end
if (!result) {
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
if (this._isWrappedRow(y)) {
continue;
}
searchPosition.startRow = y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
Expand All @@ -138,6 +144,9 @@ export class SearchEngine {
// If we hit the bottom and didn't search from the very top wrap back up
if (!result && startRow !== 0) {
for (let y = 0; y < startRow; y++) {
if (this._isWrappedRow(y)) {
continue;
}
searchPosition.startRow = y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
Expand Down Expand Up @@ -225,6 +234,15 @@ export class SearchEngine {
return result;
}

/**
* Whether a buffer row is a continuation of a wrapped line. Forward searches skip these rows
* because searching the row the wrapped line starts on already covers the entire wrapped line;
* searching each continuation row again makes the search quadratic in the wrapped line's length.
*/
private _isWrappedRow(row: number): boolean {
return this._terminal.buffer.active.getLine(row)?.isWrapped ?? false;
}

/**
* A found substring is a whole word if it doesn't have an alphanumeric character directly
* adjacent to it.
Expand Down Expand Up @@ -288,32 +306,48 @@ export class SearchEngine {
let foundTerm: RegExpExecArray | null;
if (isReverseSearch) {
// This loop will get the resultIndex of the _last_ regex match in the range 0..offset
while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
term = foundTerm[0];
searchRegex.lastIndex -= (term.length - 1);
const searchSlice = searchStringLine.slice(0, offset);
while (foundTerm = searchRegex.exec(searchSlice)) {
const matchIndex = searchRegex.lastIndex - foundTerm[0].length;
if (!searchOptions.wholeWord || this._isWholeWord(matchIndex, searchStringLine, foundTerm[0])) {
resultIndex = matchIndex;
term = foundTerm[0];
}
searchRegex.lastIndex = matchIndex + 1;
}
} else {
foundTerm = searchRegex.exec(searchStringLine.slice(offset));
if (foundTerm && foundTerm[0].length > 0) {
resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
term = foundTerm[0];
const searchSlice = searchStringLine.slice(offset);
while (foundTerm = searchRegex.exec(searchSlice)) {
if (foundTerm[0].length === 0) {
break;
}
const matchIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
if (!searchOptions.wholeWord || this._isWholeWord(matchIndex, searchStringLine, foundTerm[0])) {
resultIndex = matchIndex;
term = foundTerm[0];
break;
}
// Not a whole word, continue searching from the next character
searchRegex.lastIndex = matchIndex - offset + 1;
}
}
} else {
if (isReverseSearch) {
if (offset - searchTerm.length >= 0) {
resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);
while (searchOptions.wholeWord && resultIndex >= 0 && !this._isWholeWord(resultIndex, searchStringLine, searchTerm)) {
resultIndex = resultIndex > 0 ? searchStringLine.lastIndexOf(searchTerm, resultIndex - 1) : -1;
}
}
} else {
resultIndex = searchStringLine.indexOf(searchTerm, offset);
while (searchOptions.wholeWord && resultIndex >= 0 && !this._isWholeWord(resultIndex, searchStringLine, searchTerm)) {
resultIndex = searchStringLine.indexOf(searchTerm, resultIndex + 1);
}
}
}

if (resultIndex >= 0) {
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
return;
}

// Adjust the row number and search index if needed since a "line" of text can span multiple
// rows
Expand Down