diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index c261ca6a0e..d2ee05b4f6 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -4,6 +4,7 @@ */ import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm'; +import { EventEmitter } from 'common/EventEmitter'; export interface ISearchOptions { regex?: boolean; @@ -53,8 +54,8 @@ export class SearchAddon implements ITerminalAddon { private _dataChanged: boolean = false; private _cachedSearchTerm: string | undefined; private _selectedDecoration: IDecoration | undefined; - private _resultDecorations: Map = new Map(); - private _searchResults: Map = new Map(); + private _resultDecorations: Map | undefined; + private _searchResults: Map | undefined; private _onDataDisposable: IDisposable | undefined; private _lastSearchOptions: ISearchOptions | undefined; private _highlightTimeout: number | undefined; @@ -68,6 +69,11 @@ export class SearchAddon implements ITerminalAddon { private _cursorMoveListener: IDisposable | undefined; private _resizeListener: IDisposable | undefined; + private _resultIndex: number | undefined; + + private readonly _onDidChangeResults = new EventEmitter<{resultIndex: number, resultCount: number} | undefined>(); + public readonly onDidChangeResults = this._onDidChangeResults.event; + public activate(terminal: Terminal): void { this._terminal = terminal; this._onDataDisposable = this._terminal.onData(() => { @@ -75,11 +81,11 @@ export class SearchAddon implements ITerminalAddon { if (this._highlightTimeout) { window.clearTimeout(this._highlightTimeout); } - this._highlightTimeout = setTimeout(() => { - if (this._lastSearchOptions?.decorations && this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) { - this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions); - } - }, 200); + if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) { + this._highlightTimeout = setTimeout(() => { + this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true }); + }, 200); + } }); } @@ -90,20 +96,18 @@ export class SearchAddon implements ITerminalAddon { public clearDecorations(): void { this._selectedDecoration?.dispose(); - this._terminal?.clearSelection(); - this._searchResults.clear(); - this._disposeDecorations(); - this._cachedSearchTerm = undefined; - this._dataChanged = true; - } - - private _disposeDecorations(): void { - this._resultDecorations.forEach(decorations => { + this._searchResults?.clear(); + this._resultDecorations?.forEach(decorations => { for (const d of decorations) { d.dispose(); } }); - this._resultDecorations.clear(); + this._resultDecorations?.clear(); + this._cachedSearchTerm = undefined; + this._searchResults = undefined; + this._resultDecorations = undefined; + this._dataChanged = true; + this._resultIndex = undefined; } /** @@ -118,11 +122,18 @@ export class SearchAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } this._lastSearchOptions = searchOptions; - const findNextResult = this._findNextAndSelect(term, searchOptions); if (searchOptions?.decorations) { this._highlightAllMatches(term, searchOptions); } - return findNextResult; + const next = this._findNextAndSelect(term, searchOptions); + if (searchOptions?.decorations) { + if (next && this._resultIndex !== undefined && this._searchResults?.size) { + this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size }); + } else { + this._onDidChangeResults.fire(undefined); + } + } + return next; } private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void { @@ -137,9 +148,12 @@ export class SearchAddon implements ITerminalAddon { if (term === this._cachedSearchTerm && !this._dataChanged) { return; } + // new search, clear out the old decorations - this._disposeDecorations(); - this._searchResults.clear(); + this.clearDecorations(); + this._searchResults = new Map(); + this._resultDecorations = new Map(); + const resultDecorations = this._resultDecorations; let result = this._find(term, 0, 0, searchOptions); while (result && !this._searchResults.get(`${result.row}-${result.col}`)) { this._searchResults.set(`${result.row}-${result.col}`, result); @@ -153,9 +167,9 @@ export class SearchAddon implements ITerminalAddon { this._searchResults.forEach(result => { const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!); if (resultDecoration) { - const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; + const decorationsForLine = resultDecorations.get(resultDecoration.marker.line) || []; decorationsForLine.push(resultDecoration); - this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine); + resultDecorations.set(resultDecoration.marker.line, decorationsForLine); } }); if (this._dataChanged) { @@ -211,6 +225,7 @@ export class SearchAddon implements ITerminalAddon { return false; } + let startCol = 0; let startRow = 0; let currentSelection: ISelectionPosition | undefined; @@ -265,6 +280,17 @@ export class SearchAddon implements ITerminalAddon { result = this._findInLine(term, searchPosition, searchOptions); } + if (this._searchResults) { + if (this._resultIndex === undefined) { + this._resultIndex = 0; + } else { + this._resultIndex++; + if (this._resultIndex >= this._searchResults.size) { + this._resultIndex = 0; + } + } + } + // Set selection and scroll if a result was found return this._selectResult(result, searchOptions?.decorations); } @@ -280,14 +306,21 @@ export class SearchAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } this._lastSearchOptions = searchOptions; - const findPreviousResult = this._findAndSelectPrevious(term, searchOptions); if (searchOptions?.decorations) { this._highlightAllMatches(term, searchOptions); } - return findPreviousResult; + const previous = this._findPreviousAndSelect(term, searchOptions); + if (searchOptions?.decorations) { + if (previous && this._resultIndex !== undefined && this._searchResults?.size) { + this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size }); + } else { + this._onDidChangeResults.fire(undefined); + } + } + return previous; } - private _findAndSelectPrevious(term: string, searchOptions?: ISearchOptions): boolean { + private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } @@ -356,6 +389,17 @@ export class SearchAddon implements ITerminalAddon { } } + if (this._searchResults) { + if (this._resultIndex === undefined) { + this._resultIndex = this._searchResults?.size - 1; + } else { + this._resultIndex--; + if (this._resultIndex === -1) { + this._resultIndex = this._searchResults?.size - 1; + } + } + } + // If there is only one result, return true. if (!result && currentSelection) return true; @@ -669,7 +713,7 @@ export class SearchAddon implements ITerminalAddon { marker, x: result.col, width: result.size, - overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { + overviewRulerOptions: this._resultDecorations?.get(marker.line) && !this._dataChanged ? undefined : { color: decorations.matchOverviewRuler, position: 'center' } }); diff --git a/addons/xterm-addon-search/src/tsconfig.json b/addons/xterm-addon-search/src/tsconfig.json index d264abe07b..5a5e671fcc 100644 --- a/addons/xterm-addon-search/src/tsconfig.json +++ b/addons/xterm-addon-search/src/tsconfig.json @@ -13,10 +13,20 @@ "strict": true, "types": [ "../../../node_modules/@types/mocha" - ] + ], + "paths": { + "common/*": [ + "../../../src/common/*" + ] + } }, "include": [ "./**/*", "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + } ] } diff --git a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts index aedb6af9b9..1cb9740e60 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ILinkMatcherOptions, IDisposable, ITerminalAddon } from 'xterm'; +import { Terminal, ITerminalAddon, IEvent } from 'xterm'; declare module 'xterm-addon-search' { /** @@ -110,5 +110,12 @@ declare module 'xterm-addon-search' { * Clears the decorations and selection */ public clearDecorations(): void; + + /** + * When decorations are enabled, fires when + * the search results or the selected result changes, + * returning undefined if there are no matches. + */ + readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>; } } diff --git a/addons/xterm-addon-search/webpack.config.js b/addons/xterm-addon-search/webpack.config.js index 726dceb6f2..30526812e4 100644 --- a/addons/xterm-addon-search/webpack.config.js +++ b/addons/xterm-addon-search/webpack.config.js @@ -21,6 +21,13 @@ module.exports = { } ] }, + resolve: { + modules: ['./node_modules'], + extensions: [ '.js' ], + alias: { + common: path.resolve('../../out/common') + } + }, output: { filename: mainFile, path: path.resolve('./lib'),