From e5ffbe315a4103b7cec95c5539fd6e0d1e92c1f6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 25 May 2026 19:04:22 -0700 Subject: [PATCH] perf(addon-search): optimize regex search hot path Unify regex and whole-word matching into a single scan pass and avoid per-line substring allocations by searching the full logical line with lastIndex. Forward non-whole-word regex keeps a single-exec fast path. Co-authored-by: Cursor --- addons/addon-search/src/SearchEngine.ts | 151 +++++++++++++----------- 1 file changed, 82 insertions(+), 69 deletions(-) diff --git a/addons/addon-search/src/SearchEngine.ts b/addons/addon-search/src/SearchEngine.ts index 7fa09edf2..12c29b618 100644 --- a/addons/addon-search/src/SearchEngine.ts +++ b/addons/addon-search/src/SearchEngine.ts @@ -250,6 +250,78 @@ export class SearchEngine { (((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length]))); } + /** + * Finds a regex match in a line in a single pass, optionally enforcing whole-word boundaries. + */ + private _findRegexMatch( + searchStringLine: string, + searchRegex: RegExp, + offset: number, + isReverseSearch: boolean, + wholeWord: boolean + ): { resultIndex: number; term: string } | undefined { + let resultIndex = -1; + let matchedTerm = ''; + + if (!isReverseSearch && !wholeWord) { + searchRegex.lastIndex = offset; + const foundTerm = searchRegex.exec(searchStringLine); + if (foundTerm && foundTerm[0].length > 0) { + return { + resultIndex: searchRegex.lastIndex - foundTerm[0].length, + term: foundTerm[0] + }; + } + return undefined; + } + + searchRegex.lastIndex = 0; + if (isReverseSearch) { + let foundTerm: RegExpExecArray | null; + while (foundTerm = searchRegex.exec(searchStringLine)) { + if (foundTerm[0].length === 0) { + if (searchRegex.lastIndex >= searchStringLine.length) { + break; + } + searchRegex.lastIndex++; + continue; + } + const candidateIndex = searchRegex.lastIndex - foundTerm[0].length; + if (candidateIndex + foundTerm[0].length > offset) { + break; + } + if (!wholeWord || this._isWholeWord(candidateIndex, searchStringLine, foundTerm[0])) { + resultIndex = candidateIndex; + matchedTerm = foundTerm[0]; + } + searchRegex.lastIndex -= (foundTerm[0].length - 1); + } + } else { + searchRegex.lastIndex = offset; + let foundTerm: RegExpExecArray | null; + while (foundTerm = searchRegex.exec(searchStringLine)) { + if (foundTerm[0].length === 0) { + if (searchRegex.lastIndex >= searchStringLine.length) { + break; + } + searchRegex.lastIndex++; + continue; + } + const candidateIndex = searchRegex.lastIndex - foundTerm[0].length; + if (this._isWholeWord(candidateIndex, searchStringLine, foundTerm[0])) { + resultIndex = candidateIndex; + matchedTerm = foundTerm[0]; + break; + } + } + } + + if (resultIndex < 0) { + return undefined; + } + return { resultIndex, term: matchedTerm }; + } + /** * 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 @@ -308,29 +380,16 @@ export class SearchEngine { let resultIndex = -1; if (searchOptions.regex) { const searchRegex = lineSearchRegex ?? RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi'); - searchRegex.lastIndex = 0; - let foundTerm: RegExpExecArray | null; - if (isReverseSearch) { - // This loop will get the resultIndex of the _last_ regex match in the range 0..offset - const searchSubstring = searchStringLine.slice(0, offset); - while (foundTerm = searchRegex.exec(searchSubstring)) { - if (foundTerm[0].length === 0) { - if (searchRegex.lastIndex >= searchSubstring.length) { - break; - } - searchRegex.lastIndex++; - continue; - } - resultIndex = searchRegex.lastIndex - foundTerm[0].length; - term = foundTerm[0]; - searchRegex.lastIndex -= (term.length - 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 regexMatch = this._findRegexMatch( + searchStringLine, + searchRegex, + offset, + isReverseSearch, + !!searchOptions.wholeWord + ); + if (regexMatch) { + resultIndex = regexMatch.resultIndex; + term = regexMatch.term; } } else if (isReverseSearch) { let searchEnd = offset - searchTerm.length; @@ -361,52 +420,6 @@ export class SearchEngine { } if (resultIndex >= 0) { - if (searchOptions.regex) { - if (searchOptions.wholeWord) { - const searchRegex = lineSearchRegex ?? RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi'); - searchRegex.lastIndex = 0; - let foundTerm: RegExpExecArray | null; - if (isReverseSearch) { - resultIndex = -1; - const wholeWordSubstring = searchStringLine.slice(0, offset); - while (foundTerm = searchRegex.exec(wholeWordSubstring)) { - if (foundTerm[0].length === 0) { - if (searchRegex.lastIndex >= wholeWordSubstring.length) { - break; - } - searchRegex.lastIndex++; - 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 // rows let startRowOffset = 0;