From 11720c9dc393b477a39e63b5bfbe8574699a8f0a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 25 May 2026 17:38:35 -0700 Subject: [PATCH] fix(addon-search): continue whole-word search within same line Scan for additional matches when the first candidate fails whole-word checks instead of returning early. Co-authored-by: Cursor --- addons/addon-search/src/SearchEngine.test.ts | 11 +++ addons/addon-search/src/SearchEngine.ts | 70 +++++++++++++++++--- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/addons/addon-search/src/SearchEngine.test.ts b/addons/addon-search/src/SearchEngine.test.ts index b3cea8f54..b0054bd94 100644 --- a/addons/addon-search/src/SearchEngine.test.ts +++ b/addons/addon-search/src/SearchEngine.test.ts @@ -146,6 +146,17 @@ describe('SearchEngine', () => { assert.strictEqual(searchEngine.find('world', 0, 0, { wholeWord: true }), undefined); }); + it('should find later whole word after partial match on 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 word at beginning of line with wholeWord', async () => { await writeP(terminal, 'world is great'); diff --git a/addons/addon-search/src/SearchEngine.ts b/addons/addon-search/src/SearchEngine.ts index b9991974d..515720a31 100644 --- a/addons/addon-search/src/SearchEngine.ts +++ b/addons/addon-search/src/SearchEngine.ts @@ -300,19 +300,73 @@ export class SearchEngine { term = foundTerm[0]; } } - } else { - if (isReverseSearch) { - if (offset - searchTerm.length >= 0) { - resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length); + } else if (isReverseSearch) { + let searchEnd = offset - searchTerm.length; + while (searchEnd >= 0) { + resultIndex = searchStringLine.lastIndexOf(searchTerm, searchEnd); + if (resultIndex < 0) { + break; } - } else { - resultIndex = searchStringLine.indexOf(searchTerm, offset); + if (!searchOptions.wholeWord || this._isWholeWord(resultIndex, searchStringLine, searchTerm)) { + break; + } + searchEnd = resultIndex - 1; + resultIndex = -1; + } + } else { + let searchStart = offset; + while (searchStart <= searchStringLine.length - searchTerm.length) { + resultIndex = searchStringLine.indexOf(searchTerm, searchStart); + if (resultIndex < 0) { + break; + } + if (!searchOptions.wholeWord || this._isWholeWord(resultIndex, searchStringLine, searchTerm)) { + break; + } + searchStart = resultIndex + 1; + resultIndex = -1; } } if (resultIndex >= 0) { - if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) { - return; + if (searchOptions.regex) { + if (searchOptions.wholeWord) { + const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi'); + let foundTerm: RegExpExecArray | null; + if (isReverseSearch) { + resultIndex = -1; + while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) { + if (foundTerm[0].length === 0) { + continue; + } + const candidateIndex = searchRegex.lastIndex - foundTerm[0].length; + const candidateTerm = foundTerm[0]; + if (this._isWholeWord(candidateIndex, searchStringLine, candidateTerm)) { + resultIndex = candidateIndex; + term = candidateTerm; + } + searchRegex.lastIndex -= (candidateTerm.length - 1); + } + } else { + resultIndex = -1; + searchRegex.lastIndex = offset; + while (foundTerm = searchRegex.exec(searchStringLine)) { + if (foundTerm[0].length === 0) { + break; + } + const candidateIndex = searchRegex.lastIndex - foundTerm[0].length; + const candidateTerm = foundTerm[0]; + if (this._isWholeWord(candidateIndex, searchStringLine, candidateTerm)) { + resultIndex = candidateIndex; + term = candidateTerm; + break; + } + } + } + if (resultIndex < 0) { + return; + } + } } // Adjust the row number and search index if needed since a "line" of text can span multiple