diff --git a/addons/addon-search/src/SearchResultTracker.test.ts b/addons/addon-search/src/SearchResultTracker.test.ts index 8f9d1822c..68133fd32 100644 --- a/addons/addon-search/src/SearchResultTracker.test.ts +++ b/addons/addon-search/src/SearchResultTracker.test.ts @@ -23,6 +23,16 @@ describe('SearchResultTracker', () => { tracker.updateResults(results, 10); assert.strictEqual(tracker.searchResults, results); }); + + it('should cap results at maxResults', () => { + const results: ISearchResult[] = [ + { term: 'a', row: 0, col: 0, size: 1 }, + { term: 'b', row: 0, col: 1, size: 1 }, + { term: 'c', row: 0, col: 2, size: 1 } + ]; + tracker.updateResults(results, 2); + assert.strictEqual(tracker.searchResults.length, 2); + }); }); describe('findResultIndex', () => { @@ -50,4 +60,15 @@ describe('SearchResultTracker', () => { assert.strictEqual(fired, false); }); }); + + describe('selectedDecoration', () => { + it('should dispose the previous decoration when replaced', () => { + let disposeCount = 0; + const first = { match: { term: 'a', row: 0, col: 0, size: 1 }, dispose: () => disposeCount++ }; + const second = { match: { term: 'b', row: 0, col: 1, size: 1 }, dispose: () => {} }; + tracker.selectedDecoration = first; + tracker.selectedDecoration = second; + assert.strictEqual(disposeCount, 1); + }); + }); }); diff --git a/addons/addon-search/src/SearchResultTracker.ts b/addons/addon-search/src/SearchResultTracker.ts index c33c0062f..7747909df 100644 --- a/addons/addon-search/src/SearchResultTracker.ts +++ b/addons/addon-search/src/SearchResultTracker.ts @@ -6,7 +6,7 @@ import type { ISearchResultChangeEvent } from '@xterm/addon-search'; import type { IDisposable } from '@xterm/xterm'; import { Emitter, type IEvent } from 'common/Event'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, MutableDisposable } from 'common/Lifecycle'; import type { ISearchResult } from './SearchEngine'; /** @@ -22,7 +22,8 @@ interface ISelectedDecoration extends IDisposable { */ export class SearchResultTracker extends Disposable { private _searchResults: ISearchResult[] = []; - private _selectedDecoration: ISelectedDecoration | undefined; + private _resultIndexByKey = new Map(); + private _selectedDecoration = this._register(new MutableDisposable()); private readonly _onDidChangeResults = this._register(new Emitter()); public get onDidChangeResults(): IEvent { return this._onDidChangeResults.event; } @@ -38,14 +39,25 @@ export class SearchResultTracker extends Disposable { * Gets the currently selected decoration. */ public get selectedDecoration(): ISelectedDecoration | undefined { - return this._selectedDecoration; + return this._selectedDecoration.value; } /** * Sets the currently selected decoration. */ public set selectedDecoration(decoration: ISelectedDecoration | undefined) { - this._selectedDecoration = decoration; + this._selectedDecoration.value = decoration; + } + + private _resultKey(result: ISearchResult): string { + return `${result.row}:${result.col}:${result.size}`; + } + + private _rebuildResultIndex(): void { + this._resultIndexByKey.clear(); + for (let i = 0; i < this._searchResults.length; i++) { + this._resultIndexByKey.set(this._resultKey(this._searchResults[i]), i); + } } /** @@ -55,6 +67,7 @@ export class SearchResultTracker extends Disposable { */ public updateResults(results: ISearchResult[], maxResults: number): void { this._searchResults = results.length <= maxResults ? results : results.slice(0, maxResults); + this._rebuildResultIndex(); } /** @@ -62,16 +75,14 @@ export class SearchResultTracker extends Disposable { */ public clearResults(): void { this._searchResults = []; + this._resultIndexByKey.clear(); } /** * Clears the selected decoration. */ public clearSelectedDecoration(): void { - if (this._selectedDecoration) { - this._selectedDecoration.dispose(); - this._selectedDecoration = undefined; - } + this._selectedDecoration.clear(); } /** @@ -80,13 +91,7 @@ export class SearchResultTracker extends Disposable { * @returns The index of the result, or -1 if not found. */ public findResultIndex(result: ISearchResult): number { - for (let i = 0; i < this._searchResults.length; i++) { - const match = this._searchResults[i]; - if (match.row === result.row && match.col === result.col && match.size === result.size) { - return i; - } - } - return -1; + return this._resultIndexByKey.get(this._resultKey(result)) ?? -1; } /** @@ -99,8 +104,8 @@ export class SearchResultTracker extends Disposable { } let resultIndex = -1; - if (this._selectedDecoration) { - resultIndex = this.findResultIndex(this._selectedDecoration.match); + if (this._selectedDecoration.value) { + resultIndex = this.findResultIndex(this._selectedDecoration.value.match); } this._onDidChangeResults.fire({