diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts index 03a42619..8395e437 100644 --- a/addons/addon-search/src/SearchAddon.ts +++ b/addons/addon-search/src/SearchAddon.ts @@ -72,6 +72,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA private _cachedSearchTerm: string | undefined; private _highlightedLines: Set = new Set(); private _highlightDecorations: IHighlight[] = []; + private _searchResultsWithHighlight: ISearchResult[] = []; private _selectedDecoration: MutableDisposable = this._register(new MutableDisposable()); private _highlightLimit: number; private _lastSearchOptions: ISearchOptions | undefined; @@ -118,6 +119,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA this._selectedDecoration.clear(); dispose(this._highlightDecorations); this._highlightDecorations = []; + this._searchResultsWithHighlight = []; this._highlightedLines.clear(); if (!retainCachedSearchTerm) { this._cachedSearchTerm = undefined; @@ -167,15 +169,14 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA // new search, clear out the old decorations this.clearDecorations(true); - const searchResultsWithHighlight: ISearchResult[] = []; let prevResult: ISearchResult | undefined = undefined; let result = this._find(term, 0, 0, searchOptions); while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) { - if (searchResultsWithHighlight.length >= this._highlightLimit) { + if (this._searchResultsWithHighlight.length >= this._highlightLimit) { break; } prevResult = result; - searchResultsWithHighlight.push(prevResult); + this._searchResultsWithHighlight.push(prevResult); result = this._find( term, prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row, @@ -183,7 +184,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA searchOptions ); } - for (const match of searchResultsWithHighlight) { + for (const match of this._searchResultsWithHighlight) { const decorations = this._createResultDecorations(match, searchOptions.decorations!, false); if (decorations) { for (const decoration of decorations) { @@ -350,15 +351,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA let resultIndex = -1; if (this._selectedDecoration.value) { const selectedMatch = this._selectedDecoration.value.match; - for (let i = 0; i < this._highlightDecorations.length; i++) { - const match = this._highlightDecorations[i].match; + for (let i = 0; i < this._searchResultsWithHighlight.length; i++) { + const match = this._searchResultsWithHighlight[i]; if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) { resultIndex = i; break; } } } - this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length }); + this._onDidChangeResults.fire({ resultIndex, resultCount: this._searchResultsWithHighlight.length }); } } diff --git a/addons/addon-search/test/SearchAddon.test.ts b/addons/addon-search/test/SearchAddon.test.ts index 0045cf1c..232686b4 100644 --- a/addons/addon-search/test/SearchAddon.test.ts +++ b/addons/addon-search/test/SearchAddon.test.ts @@ -480,6 +480,57 @@ test.describe('Search Tests', () => { ]); }); }); + + test.describe('Wrapped line search functionality', () => { + test('should correctly count matches across multiple wrapped lines', async () => { + await ctx.page.evaluate(` + window.calls = []; + window.search.onDidChangeResults(e => window.calls.push(e)); + `); + + const content = 'a'.repeat(300); + await ctx.proxy.write(content); + strictEqual(await ctx.page.evaluate(`window.search.findNext('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true); + deepStrictEqual(await ctx.page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 } + ]); + }); + + test('should handle reverse search across wrapped lines', async () => { + await ctx.page.evaluate(` + window.calls = []; + window.search.onDidChangeResults(e => window.calls.push(e)); + `); + + const content = 'x'.repeat(300); + await ctx.proxy.write(content); + strictEqual(await ctx.page.evaluate(`window.search.findPrevious('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true); + deepStrictEqual(await ctx.page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 } + ]); + }); + + test('should update counts when content changes across wrapped lines', async () => { + await ctx.page.evaluate(` + window.calls = []; + window.search.onDidChangeResults(e => window.calls.push(e)); + `); + + const content = 'z'.repeat(300); + await ctx.proxy.write(content); + strictEqual(await ctx.page.evaluate(`window.search.findNext('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true); + deepStrictEqual(await ctx.page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 } + ]); + + await ctx.proxy.write('\\n\\r' + content); + await timeout(300); + deepStrictEqual(await ctx.page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 } + ]); + }); + }); }); function makeData(length: number): string {