diff --git a/addons/addon-search/src/SearchEngine.ts b/addons/addon-search/src/SearchEngine.ts index b970c0a25..eba4ac4de 100644 --- a/addons/addon-search/src/SearchEngine.ts +++ b/addons/addon-search/src/SearchEngine.ts @@ -15,6 +15,15 @@ interface ISearchPosition { startRow: number; } +interface INoMatchMemo { + term: string; + optionsKey: string; + bufferGeneration: number; + startRow: number; + startCol: number; + isReverse: boolean; +} + /** * Represents a search result with its position and content. */ @@ -42,11 +51,45 @@ const enum Constants { * This class is responsible for the actual search algorithms and position calculations. */ export class SearchEngine { + private _noMatchMemo: INoMatchMemo | undefined; + constructor( private readonly _terminal: Terminal, private readonly _lineCache: SearchLineCache ) {} + private _searchOptionsKey(searchOptions?: ISearchOptions): string { + return `${searchOptions?.caseSensitive ? 1 : 0}:${searchOptions?.regex ? 1 : 0}:${searchOptions?.wholeWord ? 1 : 0}`; + } + + private _matchesNoMatchMemo(term: string, startRow: number, startCol: number, isReverse: boolean, searchOptions?: ISearchOptions): boolean { + const memo = this._noMatchMemo; + if (!memo) { + return false; + } + return memo.term === term && + memo.optionsKey === this._searchOptionsKey(searchOptions) && + memo.bufferGeneration === this._lineCache.cacheGeneration && + memo.startRow === startRow && + memo.startCol === startCol && + memo.isReverse === isReverse; + } + + private _recordNoMatchMemo(term: string, startRow: number, startCol: number, isReverse: boolean, searchOptions?: ISearchOptions): void { + this._noMatchMemo = { + term, + optionsKey: this._searchOptionsKey(searchOptions), + bufferGeneration: this._lineCache.cacheGeneration, + startRow, + startCol, + isReverse + }; + } + + private _clearNoMatchMemo(): void { + this._noMatchMemo = undefined; + } + private _createLineSearchRegex(term: string, searchOptions?: ISearchOptions): RegExp | undefined { if (!searchOptions?.regex) { return undefined; @@ -72,8 +115,17 @@ export class SearchEngine { } this._lineCache.initLinesCache(); + if (this._matchesNoMatchMemo(term, startRow, startCol, false, searchOptions)) { + return undefined; + } const lineSearchRegex = this._createLineSearchRegex(term, searchOptions); - return this._findFromPosition(term, startRow, startCol, searchOptions, lineSearchRegex); + const result = this._findFromPosition(term, startRow, startCol, searchOptions, lineSearchRegex); + if (result) { + this._clearNoMatchMemo(); + } else { + this._recordNoMatchMemo(term, startRow, startCol, false, searchOptions); + } + return result; } /** @@ -169,6 +221,9 @@ export class SearchEngine { } this._lineCache.initLinesCache(); + if (this._matchesNoMatchMemo(term, startRow, startCol, false, searchOptions)) { + return undefined; + } const searchPosition: ISearchPosition = { startRow, @@ -209,6 +264,12 @@ export class SearchEngine { result = this._findInLine(term, searchPosition, searchOptions, false, lineSearchRegex); } + if (result) { + this._clearNoMatchMemo(); + } else { + this._recordNoMatchMemo(term, startRow, startCol, false, searchOptions); + } + return result; } @@ -233,6 +294,10 @@ export class SearchEngine { const isReverseSearch = true; this._lineCache.initLinesCache(); + if (this._matchesNoMatchMemo(term, startRow, startCol, true, searchOptions)) { + return undefined; + } + const searchPosition: ISearchPosition = { startRow, startCol @@ -279,6 +344,12 @@ export class SearchEngine { } } + if (result) { + this._clearNoMatchMemo(); + } else { + this._recordNoMatchMemo(term, startRow, startCol, true, searchOptions); + } + return result; } diff --git a/addons/addon-search/src/SearchLineCache.ts b/addons/addon-search/src/SearchLineCache.ts index 714b52203..a8a4a2ede 100644 --- a/addons/addon-search/src/SearchLineCache.ts +++ b/addons/addon-search/src/SearchLineCache.ts @@ -40,6 +40,7 @@ export class SearchLineCache extends Disposable { * _linesCache is also invalidated when the terminal cursor moves. */ private _linesCache: LineCacheEntry[] | undefined; + private _cacheGeneration = 0; private _linesCacheTimeout = this._register(new MutableDisposable()); private _linesCacheDisposables = this._register(new MutableDisposable()); // Track access to avoid recreating a timeout on every init call which occurs once per search @@ -70,7 +71,12 @@ export class SearchLineCache extends Disposable { } } + public get cacheGeneration(): number { + return this._cacheGeneration; + } + private _destroyLinesCache(): void { + this._cacheGeneration++; this._linesCache = undefined; this._lastAccessTimestamp = 0; this._linesCacheDisposables.clear();