From 60c11e87f67919d56a6d0314c01dfd60f4562c85 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 10 Mar 2022 23:40:00 -0500 Subject: [PATCH 01/55] start work --- addons/xterm-addon-search/src/SearchAddon.ts | 130 +++++-------------- typings/xterm.d.ts | 2 +- 2 files changed, 33 insertions(+), 99 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 4651f6c7f4..4a1a6ef03d 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, IBufferLine, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm'; +import { Terminal, IBufferLine, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm'; export interface ISearchOptions { regex?: boolean; @@ -40,7 +40,7 @@ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs export class SearchAddon implements ITerminalAddon { private _terminal: Terminal | undefined; - + private _resultDecorations: IDecoration[] = []; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -130,7 +130,7 @@ export class SearchAddon implements ITerminalAddon { } // Set selection and scroll if a result was found - return this._selectResult(result); + return true; } /** @@ -149,69 +149,23 @@ export class SearchAddon implements ITerminalAddon { this._terminal.clearSelection(); return false; } - - const isReverseSearch = true; - let startRow = this._terminal.buffer.active.baseY + this._terminal.rows; - let startCol = this._terminal.cols; - let result: ISearchResult | undefined; - const incremental = searchOptions ? searchOptions.incremental : false; - let currentSelection: ISelectionPosition | undefined; - if (this._terminal.hasSelection()) { - currentSelection = this._terminal.getSelectionPosition()!; - // Start from selection start if there is a selection - startRow = currentSelection.startRow; - startCol = currentSelection.startColumn; - } - - this._initLinesCache(); - const searchPosition: ISearchPosition = { - startRow, - startCol - }; - - if (incremental) { - // Try to expand selection to right first. - result = this._findInLine(term, searchPosition, searchOptions, false); - const isOldResultHighlighted = result && result.row === startRow && result.col === startCol; - if (!isOldResultHighlighted) { - // If selection was not able to be expanded to the right, then try reverse search - if (currentSelection) { - searchPosition.startRow = currentSelection.endRow; - searchPosition.startCol = currentSelection.endColumn; - } - result = this._findInLine(term, searchPosition, searchOptions, true); + const results = []; + for (let i = this._terminal.buffer.active.viewportY; i < this._terminal.buffer.active.viewportY + this._terminal.rows; i++) { + const result = this._findInLine(term, { startCol: 0, startRow: i }, searchOptions); + if (result) { + results.push(result); } - } else { - result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); } - - // Search from startRow - 1 to top - if (!result) { - searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols); - for (let y = startRow - 1; y >= 0; y--) { - searchPosition.startRow = y; - result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); - if (result) { - break; - } + for (const result of results.filter(r => !!r && r.term.length)) { + const resultDecoration = this._showResultDecoration(result); + if (resultDecoration) { + // Add decoration + this._resultDecorations.push(resultDecoration); } } - // If we hit the top and didn't search from the very bottom wrap back down - if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) { - for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) { - searchPosition.startRow = y; - result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); - if (result) { - break; - } - } - } - - // If there is only one result, return true. - if (!result && currentSelection) return true; - - // Set selection and scroll if a result was found - return this._selectResult(result); + console.log(results); + console.log(this._resultDecorations); + return true; } /** @@ -267,7 +221,7 @@ export class SearchAddon implements ITerminalAddon { * @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left. * @return The search result if it was found. */ - protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { + protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}): ISearchResult | undefined { const terminal = this._terminal!; const row = searchPosition.startRow; const col = searchPosition.startCol; @@ -275,10 +229,6 @@ export class SearchAddon implements ITerminalAddon { // Ignore wrapped lines, only consider on unwrapped line (first row of command string). const firstLine = terminal.buffer.active.getLine(row); if (firstLine?.isWrapped) { - if (isReverseSearch) { - searchPosition.startCol += terminal.cols; - return; - } // This will iterate until we find the line start. // When we find it, we will search using the calculated start column. @@ -302,29 +252,13 @@ export class SearchAddon implements ITerminalAddon { let resultIndex = -1; if (searchOptions.regex) { const searchRegex = RegExp(searchTerm, 'g'); - let foundTerm: RegExpExecArray | null; - if (isReverseSearch) { - // This loop will get the resultIndex of the _last_ regex match in the range 0..offset - while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) { - 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 foundTerm = searchRegex.exec(searchStringLine.slice(offset)); + if (foundTerm && foundTerm[0].length > 0) { + resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length); + term = foundTerm[0]; } } else { - if (isReverseSearch) { - if (offset - searchTerm.length >= 0) { - resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length); - } - } else { - resultIndex = searchStringLine.indexOf(searchTerm, offset); - } + resultIndex = searchStringLine.indexOf(searchTerm, offset); } if (resultIndex >= 0) { @@ -448,19 +382,19 @@ export class SearchAddon implements ITerminalAddon { * @param result The result to select. * @return Whethera result was selected. */ - private _selectResult(result: ISearchResult | undefined): boolean { + private _showResultDecoration(result: ISearchResult | undefined): IDecoration | undefined { const terminal = this._terminal!; - if (!result) { + if (!result || result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { terminal.clearSelection(); - return false; + return; } - terminal.select(result.col, result.row, result.size); - // If it is not in the viewport then we scroll else it just gets selected - if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { - let scroll = result.row - terminal.buffer.active.viewportY; - scroll -= Math.floor(terminal.rows / 2); - terminal.scrollLines(scroll); + // TODO: + const marker = terminal.registerMarker(undefined, result.col, result.row); + if (!marker) { + return undefined; } - return true; + const findResultDecoration = terminal.registerDecoration({ marker, width: result.size }); + findResultDecoration?.onRender((e) => console.log('rendered', e, result?.term, result?.col)); + return findResultDecoration; } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2cd4daa617..44106354b3 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -930,7 +930,7 @@ declare module 'xterm' { * @param cursorYOffset The y position offset of the marker from the cursor. * @returns The new marker or undefined. */ - registerMarker(cursorYOffset?: number): IMarker | undefined; + registerMarker(cursorYOffset?: number, col?: number, row?: number): IMarker | undefined; /** * @deprecated use `registerMarker` instead. From 96574425c7a6363b4ec031ff821b36d90d05962f Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 11 Mar 2022 00:39:18 -0500 Subject: [PATCH 02/55] get it to sort of work --- addons/xterm-addon-search/src/SearchAddon.ts | 14 +++++++++++--- src/browser/Terminal.ts | 7 +++++-- src/browser/Types.d.ts | 2 +- src/browser/public/Terminal.ts | 7 +++++-- typings/xterm.d.ts | 2 +- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 4a1a6ef03d..1f1ff96163 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -144,6 +144,8 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } + this._resultDecorations.forEach(d => d.dispose()); + this._resultDecorations = []; if (!term || term.length === 0) { this._terminal.clearSelection(); @@ -388,13 +390,19 @@ export class SearchAddon implements ITerminalAddon { terminal.clearSelection(); return; } - // TODO: - const marker = terminal.registerMarker(undefined, result.col, result.row); + const marker = terminal.registerMarker(undefined, result.row - 1); if (!marker) { return undefined; } const findResultDecoration = terminal.registerDecoration({ marker, width: result.size }); - findResultDecoration?.onRender((e) => console.log('rendered', e, result?.term, result?.col)); + findResultDecoration?.onRender((e) => { + console.log('rendered', e, result?.term, result?.row); + e.style.backgroundColor = 'blue'; + e.style.color = 'white'; + e.style.opacity = '60%'; + // TODO: use cell width here instead of 10 + e.style.left = `${(result.col === 0 ? 0 : result.col - 1) * 10}px`; + }); return findResultDecoration; } } diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 089639338d..69d5bfaded 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -993,12 +993,15 @@ export class Terminal extends CoreTerminal implements ITerminal { return this.buffer.markers; } - public addMarker(cursorYOffset: number): IMarker | undefined { + public addMarker(cursorYOffset: number, row?: number): IMarker | undefined { // Disallow markers on the alt buffer if (this.buffer !== this.buffers.normal) { return; } - + if (row) { + console.log(row); + return this.buffer.addMarker(row + 1); + } return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset); } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 35b52d6282..c66b09286c 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -60,7 +60,7 @@ export interface IPublicTerminal extends IDisposable { registerLinkProvider(linkProvider: ILinkProvider): IDisposable; registerCharacterJoiner(handler: (text: string) => [number, number][]): number; deregisterCharacterJoiner(joinerId: number): void; - addMarker(cursorYOffset: number): IMarker | undefined; + addMarker(cursorYOffset: number, col?: number, row?: number): IMarker | undefined; registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; hasSelection(): boolean; getSelection(): string; diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 1acde934d8..8c290fda0e 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -166,10 +166,13 @@ export class Terminal implements ITerminalApi { this._checkProposedApi(); this._core.deregisterCharacterJoiner(joinerId); } - public registerMarker(cursorYOffset: number = 0): IMarker | undefined { + public registerMarker(cursorYOffset: number = 0, row?: number): IMarker | undefined { this._checkProposedApi(); this._verifyIntegers(cursorYOffset); - return this._core.addMarker(cursorYOffset); + if (row) { + this._verifyPositiveIntegers(row); + } + return this._core.addMarker(cursorYOffset, row); } public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { this._checkProposedApi(); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 44106354b3..cc6dc8adb4 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -930,7 +930,7 @@ declare module 'xterm' { * @param cursorYOffset The y position offset of the marker from the cursor. * @returns The new marker or undefined. */ - registerMarker(cursorYOffset?: number, col?: number, row?: number): IMarker | undefined; + registerMarker(cursorYOffset?: number, row?: number): IMarker | undefined; /** * @deprecated use `registerMarker` instead. From fa4479c79724b2dffcce2ce15ce8d52a6fc8bdf0 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 11 Mar 2022 12:01:19 -0500 Subject: [PATCH 03/55] use decoration width, find all and select next --- addons/xterm-addon-search/src/SearchAddon.ts | 208 ++++++++++++++---- .../typings/xterm-addon-search.d.ts | 18 +- demo/client.ts | 5 + demo/index.html | 1 + src/browser/Terminal.ts | 3 +- 5 files changed, 189 insertions(+), 46 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 1f1ff96163..d26dd6be9e 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, IBufferLine, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm'; +import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm'; export interface ISearchOptions { regex?: boolean; @@ -41,6 +41,7 @@ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs export class SearchAddon implements ITerminalAddon { private _terminal: Terminal | undefined; private _resultDecorations: IDecoration[] = []; + private _result: ISearchResult | undefined; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -57,6 +58,52 @@ export class SearchAddon implements ITerminalAddon { public dispose(): void { } + /** + * Find all instances of the term, selecting the next one with each + * enter. If it doesn't exist, do nothing. + * @param term The search term. + * @param searchOptions Search options. + * @return Whether a result was found. + */ + public find(term: string, searchOptions?: ISearchOptions): boolean { + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } + + if (!term || term.length === 0) { + this._terminal.clearSelection(); + this._resultDecorations.forEach(d => d.dispose()); + this._resultDecorations = []; + return false; + } + + // new search, clear out the old decorations + this._resultDecorations.forEach(d => d.dispose()); + this._resultDecorations = []; + + const results: ISearchResult[] = []; + let found = this.findNext(term, searchOptions); + while (found && !results.find(r => r?.col === this._result?.col && r?.row === this._result?.row)) { + if (this._result) { + results.push(this._result); + } + found = this.findNext(term, searchOptions); + } + + for (const result of results) { + const resultDecoration = this._showResultDecoration(result); + if (resultDecoration) { + // Add decoration + this._resultDecorations.push(resultDecoration); + } + } + if (results.length > 0) { + // this.findNext(term, searchOptions); + } + return true; + } + + /** * Find the next instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. @@ -70,6 +117,7 @@ export class SearchAddon implements ITerminalAddon { } if (!term || term.length === 0) { + this._result = undefined; this._terminal.clearSelection(); return false; } @@ -94,43 +142,42 @@ export class SearchAddon implements ITerminalAddon { }; // Search startRow - let result = this._findInLine(term, searchPosition, searchOptions); - + this._result = this._findInLine(term, searchPosition, searchOptions); // Search from startRow + 1 to end - if (!result) { + if (!this._result) { for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) { searchPosition.startRow = y; searchPosition.startCol = 0; // If the current line is wrapped line, increase index of column to ignore the previous scan // Otherwise, reset beginning column index to zero with set new unwrapped line index - result = this._findInLine(term, searchPosition, searchOptions); - if (result) { + this._result = this._findInLine(term, searchPosition, searchOptions); + if (this._result) { break; } } } // If we hit the bottom and didn't search from the very top wrap back up - if (!result && startRow !== 0) { + if (!this._result && startRow !== 0) { for (let y = 0; y < startRow; y++) { searchPosition.startRow = y; searchPosition.startCol = 0; - result = this._findInLine(term, searchPosition, searchOptions); - if (result) { + this._result = this._findInLine(term, searchPosition, searchOptions); + if (this._result) { break; } } } // If there is only one result, wrap back and return selection if it exists. - if (!result && currentSelection) { + if (!this._result && currentSelection) { searchPosition.startRow = currentSelection.startRow; searchPosition.startCol = 0; - result = this._findInLine(term, searchPosition, searchOptions); + this._result = this._findInLine(term, searchPosition, searchOptions); } // Set selection and scroll if a result was found - return true; + return this._selectResult(this._result); } /** @@ -144,32 +191,77 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - this._resultDecorations.forEach(d => d.dispose()); - this._resultDecorations = []; if (!term || term.length === 0) { this._terminal.clearSelection(); return false; } - const results = []; - for (let i = this._terminal.buffer.active.viewportY; i < this._terminal.buffer.active.viewportY + this._terminal.rows; i++) { - const result = this._findInLine(term, { startCol: 0, startRow: i }, searchOptions); - if (result) { - results.push(result); + + const isReverseSearch = true; + let startRow = this._terminal.buffer.active.baseY + this._terminal.rows; + let startCol = this._terminal.cols; + let result: ISearchResult | undefined; + const incremental = searchOptions ? searchOptions.incremental : false; + let currentSelection: ISelectionPosition | undefined; + if (this._terminal.hasSelection()) { + currentSelection = this._terminal.getSelectionPosition()!; + // Start from selection start if there is a selection + startRow = currentSelection.startRow; + startCol = currentSelection.startColumn; + } + + this._initLinesCache(); + const searchPosition: ISearchPosition = { + startRow, + startCol + }; + + if (incremental) { + // Try to expand selection to right first. + result = this._findInLine(term, searchPosition, searchOptions, false); + const isOldResultHighlighted = result && result.row === startRow && result.col === startCol; + if (!isOldResultHighlighted) { + // If selection was not able to be expanded to the right, then try reverse search + if (currentSelection) { + searchPosition.startRow = currentSelection.endRow; + searchPosition.startCol = currentSelection.endColumn; + } + result = this._findInLine(term, searchPosition, searchOptions, true); } + } else { + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); } - for (const result of results.filter(r => !!r && r.term.length)) { - const resultDecoration = this._showResultDecoration(result); - if (resultDecoration) { - // Add decoration - this._resultDecorations.push(resultDecoration); + + // Search from startRow - 1 to top + if (!result) { + searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols); + for (let y = startRow - 1; y >= 0; y--) { + searchPosition.startRow = y; + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); + if (result) { + break; + } } } - console.log(results); - console.log(this._resultDecorations); - return true; + // If we hit the top and didn't search from the very bottom wrap back down + if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) { + for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) { + searchPosition.startRow = y; + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); + if (result) { + break; + } + } + } + + // If there is only one result, return true. + if (!result && currentSelection) return true; + + // Set selection and scroll if a result was found + return this._selectResult(result); } + /** * Sets up a line cache with a ttl */ @@ -223,7 +315,7 @@ export class SearchAddon implements ITerminalAddon { * @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left. * @return The search result if it was found. */ - protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}): ISearchResult | undefined { + protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { const terminal = this._terminal!; const row = searchPosition.startRow; const col = searchPosition.startCol; @@ -231,6 +323,10 @@ export class SearchAddon implements ITerminalAddon { // Ignore wrapped lines, only consider on unwrapped line (first row of command string). const firstLine = terminal.buffer.active.getLine(row); if (firstLine?.isWrapped) { + if (isReverseSearch) { + searchPosition.startCol += terminal.cols; + return; + } // This will iterate until we find the line start. // When we find it, we will search using the calculated start column. @@ -254,13 +350,29 @@ export class SearchAddon implements ITerminalAddon { let resultIndex = -1; if (searchOptions.regex) { const searchRegex = RegExp(searchTerm, 'g'); - const foundTerm = searchRegex.exec(searchStringLine.slice(offset)); - if (foundTerm && foundTerm[0].length > 0) { - resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length); - term = foundTerm[0]; + let foundTerm: RegExpExecArray | null; + if (isReverseSearch) { + // This loop will get the resultIndex of the _last_ regex match in the range 0..offset + while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) { + 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]; + } } } else { - resultIndex = searchStringLine.indexOf(searchTerm, offset); + if (isReverseSearch) { + if (offset - searchTerm.length >= 0) { + resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length); + } + } else { + resultIndex = searchStringLine.indexOf(searchTerm, offset); + } } if (resultIndex >= 0) { @@ -384,24 +496,42 @@ export class SearchAddon implements ITerminalAddon { * @param result The result to select. * @return Whethera result was selected. */ + private _selectResult(result: ISearchResult | undefined): boolean { + const terminal = this._terminal!; + if (!result) { + terminal.clearSelection(); + return false; + } + terminal.select(result.col, result.row, result.size); + // If it is not in the viewport then we scroll else it just gets selected + if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { + let scroll = result.row - terminal.buffer.active.viewportY; + scroll -= Math.floor(terminal.rows / 2); + terminal.scrollLines(scroll); + } + return true; + } + + /** + * Registers a decoration for the @param result + * and @returns the decoration or undefined if + * the marker has already been disposed of + */ private _showResultDecoration(result: ISearchResult | undefined): IDecoration | undefined { const terminal = this._terminal!; - if (!result || result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { + if (!result) { terminal.clearSelection(); return; } - const marker = terminal.registerMarker(undefined, result.row - 1); + const marker = terminal.registerMarker(undefined, result.row); if (!marker) { return undefined; } const findResultDecoration = terminal.registerDecoration({ marker, width: result.size }); findResultDecoration?.onRender((e) => { - console.log('rendered', e, result?.term, result?.row); e.style.backgroundColor = 'blue'; - e.style.color = 'white'; e.style.opacity = '60%'; - // TODO: use cell width here instead of 10 - e.style.left = `${(result.col === 0 ? 0 : result.col - 1) * 10}px`; + e.style.left = `${result.col * e.clientWidth}px`; }); return findResultDecoration; } 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 f27aba7834..228a5ffeb2 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -50,19 +50,27 @@ declare module 'xterm-addon-search' { public dispose(): void; /** - * Search forwards for the next result that matches the search term and - * options. + * Find all instances of the term, selecting the next one with each + * enter. If it doesn't exist, do nothing. * @param term The search term. * @param searchOptions The options for the search. */ - public findNext(term: string, searchOptions?: ISearchOptions): boolean; + public find(term: string, searchOptions?: ISearchOptions): boolean; /** - * Search backwards for the previous result that matches the search term and + * Search forwards for the next result that matches the search term and * options. * @param term The search term. * @param searchOptions The options for the search. */ - public findPrevious(term: string, searchOptions?: ISearchOptions): boolean; + public findNext(term: string, searchOptions?: ISearchOptions): boolean; + + /** + * Search backwards for the previous result that matches the search term and + * options. + * @param term The search term. + * @param searchOptions The options for the search. + */ + public findPrevious(term: string, searchOptions?: ISearchOptions): boolean; } } diff --git a/demo/client.ts b/demo/client.ts index aee23401a0..65bdc6ce9e 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -92,6 +92,7 @@ const addons: { [T in AddonType]: IDemoAddon} = { const terminalContainer = document.getElementById('terminal-container'); const actionElements = { + find: document.querySelector('#find'), findNext: document.querySelector('#find-next'), findPrevious: document.querySelector('#find-previous') }; @@ -199,6 +200,10 @@ function createTerminal(): void { addDomListener(paddingElement, 'change', setPadding); + addDomListener(actionElements.find, 'keyup', (e) => { + addons.search.instance.find(actionElements.find.value, getSearchOptions(e)); + }); + addDomListener(actionElements.findNext, 'keyup', (e) => { addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e)); }); diff --git a/demo/index.html b/demo/index.html index e024222c46..590bf1e68f 100644 --- a/demo/index.html +++ b/demo/index.html @@ -38,6 +38,7 @@

Addons

Addons Control

SearchAddon

+ diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 69d5bfaded..4d546dcb00 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -999,8 +999,7 @@ export class Terminal extends CoreTerminal implements ITerminal { return; } if (row) { - console.log(row); - return this.buffer.addMarker(row + 1); + return this.buffer.addMarker(row); } return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset); } From 372b6b633b6d0b291775482d8dc7c691a1f34329 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 11 Mar 2022 12:13:43 -0500 Subject: [PATCH 04/55] set incremental search to false --- addons/xterm-addon-search/src/SearchAddon.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index d26dd6be9e..a06f32e053 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -80,8 +80,9 @@ export class SearchAddon implements ITerminalAddon { // new search, clear out the old decorations this._resultDecorations.forEach(d => d.dispose()); this._resultDecorations = []; - const results: ISearchResult[] = []; + searchOptions = searchOptions || {}; + searchOptions.incremental = false; let found = this.findNext(term, searchOptions); while (found && !results.find(r => r?.col === this._result?.col && r?.row === this._result?.row)) { if (this._result) { @@ -527,11 +528,12 @@ export class SearchAddon implements ITerminalAddon { if (!marker) { return undefined; } - const findResultDecoration = terminal.registerDecoration({ marker, width: result.size }); + const findResultDecoration = terminal.registerDecoration({ marker }); findResultDecoration?.onRender((e) => { e.style.backgroundColor = 'blue'; e.style.opacity = '60%'; e.style.left = `${result.col * e.clientWidth}px`; + e.style.width = `${e.clientWidth * result.term.length}px`; }); return findResultDecoration; } From da2c59d1d0450d5a1f1677c2d323c642e5c8d938 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 11 Mar 2022 12:24:36 -0500 Subject: [PATCH 05/55] remove unused variable --- addons/xterm-addon-search/src/SearchAddon.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index a06f32e053..e28b409a33 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -42,6 +42,8 @@ export class SearchAddon implements ITerminalAddon { private _terminal: Terminal | undefined; private _resultDecorations: IDecoration[] = []; private _result: ISearchResult | undefined; + private _reset: boolean = false; + private _cachedSearchTerm: string | undefined; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -54,6 +56,7 @@ export class SearchAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; + this._terminal.onData(() => this._reset = true); } public dispose(): void { } @@ -77,6 +80,12 @@ export class SearchAddon implements ITerminalAddon { return false; } + if (!this._reset && term === this._cachedSearchTerm) { + return this.findNext(term, searchOptions); + } + this._reset = false; + + // new search, clear out the old decorations this._resultDecorations.forEach(d => d.dispose()); this._resultDecorations = []; @@ -99,7 +108,7 @@ export class SearchAddon implements ITerminalAddon { } } if (results.length > 0) { - // this.findNext(term, searchOptions); + this._cachedSearchTerm = term; } return true; } From 453cb00807cb501dac96194006ac3a86a8de41cf Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 11 Mar 2022 12:43:09 -0500 Subject: [PATCH 06/55] use a class --- addons/xterm-addon-search/src/SearchAddon.ts | 10 ++++++---- css/xterm.css | 5 +++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index e28b409a33..e50fdd178e 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -539,10 +539,12 @@ export class SearchAddon implements ITerminalAddon { } const findResultDecoration = terminal.registerDecoration({ marker }); findResultDecoration?.onRender((e) => { - e.style.backgroundColor = 'blue'; - e.style.opacity = '60%'; - e.style.left = `${result.col * e.clientWidth}px`; - e.style.width = `${e.clientWidth * result.term.length}px`; + if (!e.classList.contains('xterm-find-result-decoration')) { + e.classList.add('xterm-find-result-decoration'); + // decoration's clientWidth = actualCellWidth + e.style.left = `${e.clientWidth * result.col}px`; + e.style.width = `${e.clientWidth * result.term.length}px`; + } }); return findResultDecoration; } diff --git a/css/xterm.css b/css/xterm.css index ab3965b488..ded9307461 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -178,3 +178,8 @@ z-index: 6; position: absolute; } + +.xterm-find-result-decoration { + background-color: blue; + opacity: 60%; +} \ No newline at end of file From e7bbc4151afb795770e529a46de265b8764d38c9 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 11 Mar 2022 13:10:26 -0500 Subject: [PATCH 07/55] start at cursor position and mandate a width gt 0 --- addons/xterm-addon-search/src/SearchAddon.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index e50fdd178e..5e0895949b 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -101,10 +101,11 @@ export class SearchAddon implements ITerminalAddon { } for (const result of results) { - const resultDecoration = this._showResultDecoration(result); - if (resultDecoration) { - // Add decoration - this._resultDecorations.push(resultDecoration); + if (result) { + const resultDecoration = this._showResultDecoration(result); + if (resultDecoration) { + this._resultDecorations.push(resultDecoration); + } } } if (results.length > 0) { @@ -142,6 +143,9 @@ export class SearchAddon implements ITerminalAddon { currentSelection = this._terminal.getSelectionPosition()!; startRow = incremental ? currentSelection.startRow : currentSelection.endRow; startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn; + } else { + startRow = this._terminal.buffer.active.cursorY; + startCol = this._terminal.buffer.active.cursorX; } this._initLinesCache(); @@ -527,19 +531,15 @@ export class SearchAddon implements ITerminalAddon { * and @returns the decoration or undefined if * the marker has already been disposed of */ - private _showResultDecoration(result: ISearchResult | undefined): IDecoration | undefined { + private _showResultDecoration(result: ISearchResult): IDecoration | undefined { const terminal = this._terminal!; - if (!result) { - terminal.clearSelection(); - return; - } const marker = terminal.registerMarker(undefined, result.row); if (!marker) { return undefined; } const findResultDecoration = terminal.registerDecoration({ marker }); findResultDecoration?.onRender((e) => { - if (!e.classList.contains('xterm-find-result-decoration')) { + if (!e.classList.contains('xterm-find-result-decoration') && result.term.length && e.clientWidth > 0) { e.classList.add('xterm-find-result-decoration'); // decoration's clientWidth = actualCellWidth e.style.left = `${e.clientWidth * result.col}px`; From b4de2787c7d0734774efcbe3d365bdcc5e1e258f Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 11 Mar 2022 13:13:05 -0500 Subject: [PATCH 08/55] hide decorations >= bufferService rows --- src/browser/services/DecorationService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/services/DecorationService.ts b/src/browser/services/DecorationService.ts index ed3c722447..a77409dbef 100644 --- a/src/browser/services/DecorationService.ts +++ b/src/browser/services/DecorationService.ts @@ -142,7 +142,7 @@ export class Decoration extends Disposable implements IDecoration { return; } const line = this.marker.line - this._bufferService.buffers.active.ydisp; - if (line < 0 || line > this._bufferService.rows) { + if (line < 0 || line >= this._bufferService.rows) { // outside of viewport this._element.style.display = 'none'; } else { From 1bab173e99885eb70e70e3a0c7396c99fa55c6b6 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 16 Mar 2022 12:45:18 -0400 Subject: [PATCH 09/55] fix problems --- addons/xterm-addon-search/src/SearchAddon.ts | 2 +- src/browser/services/DecorationService.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 5e0895949b..104ca53240 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -537,7 +537,7 @@ export class SearchAddon implements ITerminalAddon { if (!marker) { return undefined; } - const findResultDecoration = terminal.registerDecoration({ marker }); + const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'yellow' } }); findResultDecoration?.onRender((e) => { if (!e.classList.contains('xterm-find-result-decoration') && result.term.length && e.clientWidth > 0) { e.classList.add('xterm-find-result-decoration'); diff --git a/src/browser/services/DecorationService.ts b/src/browser/services/DecorationService.ts index a77409dbef..80f7f0d819 100644 --- a/src/browser/services/DecorationService.ts +++ b/src/browser/services/DecorationService.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { IDecorationService, IRenderService } from 'browser/services/Services'; +import { IRenderService } from 'browser/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; -import { IBufferService, IInstantiationService } from 'common/services/Services'; +import { IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services'; import { IDecorationOptions, IDecoration, IMarker } from 'xterm'; export class DecorationService extends Disposable implements IDecorationService { From 40b8854df4fcb2de238a8213f3ab168e9ae18a69 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 16 Mar 2022 12:47:46 -0400 Subject: [PATCH 10/55] rm bad decoration service --- src/browser/services/DecorationService.ts | 164 ---------------------- 1 file changed, 164 deletions(-) delete mode 100644 src/browser/services/DecorationService.ts diff --git a/src/browser/services/DecorationService.ts b/src/browser/services/DecorationService.ts deleted file mode 100644 index 80f7f0d819..0000000000 --- a/src/browser/services/DecorationService.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright (c) 2022 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IRenderService } from 'browser/services/Services'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { Disposable } from 'common/Lifecycle'; -import { IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services'; -import { IDecorationOptions, IDecoration, IMarker } from 'xterm'; - -export class DecorationService extends Disposable implements IDecorationService { - - private readonly _decorations: Decoration[] = []; - private _container: HTMLElement | undefined; - private _screenElement: HTMLElement | undefined; - private _renderService: IRenderService | undefined; - private _animationFrame: number | undefined; - - constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { super(); } - - public attachToDom(screenElement: HTMLElement, renderService: IRenderService): void { - this._renderService = renderService; - this._screenElement = screenElement; - this._container = document.createElement('div'); - this._container.classList.add('xterm-decoration-container'); - screenElement.appendChild(this._container); - this.register(this._renderService.onRenderedBufferChange(() => this.refresh())); - this.register(this._renderService.onDimensionsChange(() => this.refresh(true))); - } - - public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { - if (decorationOptions.marker.isDisposed || !this._container) { - return undefined; - } - const decoration = this._instantiationService.createInstance(Decoration, decorationOptions, this._container); - this._decorations.push(decoration); - decoration.onDispose(() => this._decorations.splice(this._decorations.indexOf(decoration), 1)); - this._queueRefresh(); - return decoration; - } - - private _queueRefresh(): void { - if (this._animationFrame !== undefined) { - return; - } - this._animationFrame = window.requestAnimationFrame(() => { - this.refresh(); - this._animationFrame = undefined; - }); - } - - public refresh(shouldRecreate?: boolean): void { - if (!this._renderService) { - return; - } - for (const decoration of this._decorations) { - decoration.render(this._renderService, shouldRecreate); - } - } - - public dispose(): void { - for (const decoration of this._decorations) { - decoration.dispose(); - } - if (this._screenElement && this._container && this._screenElement.contains(this._container)) { - this._screenElement.removeChild(this._container); - } - } -} -export class Decoration extends Disposable implements IDecoration { - private readonly _marker: IMarker; - private _element: HTMLElement | undefined; - - public isDisposed: boolean = false; - - public get element(): HTMLElement | undefined { return this._element; } - public get marker(): IMarker { return this._marker; } - - private _onDispose = new EventEmitter(); - public get onDispose(): IEvent { return this._onDispose.event; } - - private _onRender = new EventEmitter(); - public get onRender(): IEvent { return this._onRender.event; } - - public x: number; - public anchor: 'left' | 'right'; - public width: number; - public height: number; - - constructor( - options: IDecorationOptions, - private readonly _container: HTMLElement, - @IBufferService private readonly _bufferService: IBufferService - ) { - super(); - this.x = options.x ?? 0; - this._marker = options.marker; - this._marker.onDispose(() => this.dispose()); - this.anchor = options.anchor || 'left'; - this.width = options.width || 1; - this.height = options.height || 1; - } - - public render(renderService: IRenderService, shouldRecreate?: boolean): void { - if (!this._element || shouldRecreate) { - this._createElement(renderService, shouldRecreate); - } - if (this._container && this._element && !this._container.contains(this._element)) { - this._container.append(this._element); - } - this._refreshStyle(renderService); - if (this._element) { - this._onRender.fire(this._element); - } - } - - private _createElement(renderService: IRenderService, shouldRecreate?: boolean): void { - if (shouldRecreate && this._element && this._container.contains(this._element)) { - this._container.removeChild(this._element); - } - this._element = document.createElement('div'); - this._element.classList.add('xterm-decoration'); - this._element.style.width = `${this.width * renderService.dimensions.actualCellWidth}px`; - this._element.style.height = `${this.height * renderService.dimensions.actualCellHeight}px`; - this._element.style.top = `${(this.marker.line - this._bufferService.buffers.active.ydisp) * renderService.dimensions.actualCellHeight}px`; - this._element.style.lineHeight = `${renderService.dimensions.actualCellHeight}px`; - - if (this.x && this.x > this._bufferService.cols) { - // exceeded the container width, so hide - this._element.style.display = 'none'; - } - if (this.anchor === 'right') { - this._element.style.right = this.x ? `${this.x * renderService.dimensions.actualCellWidth}px` : ''; - } else { - this._element.style.left = this.x ? `${this.x * renderService.dimensions.actualCellWidth}px` : ''; - } - } - - private _refreshStyle(renderService: IRenderService): void { - if (!this._element) { - return; - } - const line = this.marker.line - this._bufferService.buffers.active.ydisp; - if (line < 0 || line >= this._bufferService.rows) { - // outside of viewport - this._element.style.display = 'none'; - } else { - this._element.style.top = `${line * renderService.dimensions.actualCellHeight}px`; - this._element.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block'; - } - } - - public override dispose(): void { - if (this.isDisposed) { - return; - } - if (this._element && this._container.contains(this._element)) { - this._container.removeChild(this._element); - } - this.isDisposed = true; - this._onDispose.fire(); - } -} From 05eb12e6dd051c7c845e524a6d2b05e16ade2f4b Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 16 Mar 2022 13:03:21 -0400 Subject: [PATCH 11/55] start work on selection color --- addons/xterm-addon-search/src/SearchAddon.ts | 24 ++++++++++++++------ css/xterm.css | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 104ca53240..1e54ee3bd6 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -44,6 +44,7 @@ export class SearchAddon implements ITerminalAddon { private _result: ISearchResult | undefined; private _reset: boolean = false; private _cachedSearchTerm: string | undefined; + private _cachedResults: ISearchResult[] = []; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -81,6 +82,11 @@ export class SearchAddon implements ITerminalAddon { } if (!this._reset && term === this._cachedSearchTerm) { + // this._resultDecorations.forEach(d => d.dispose()); + // this._resultDecorations = []; + // for (const decoration of this._cachedResults) { + // this._showResultDecoration(decoration); + // } return this.findNext(term, searchOptions); } this._reset = false; @@ -89,18 +95,17 @@ export class SearchAddon implements ITerminalAddon { // new search, clear out the old decorations this._resultDecorations.forEach(d => d.dispose()); this._resultDecorations = []; - const results: ISearchResult[] = []; searchOptions = searchOptions || {}; searchOptions.incremental = false; let found = this.findNext(term, searchOptions); - while (found && !results.find(r => r?.col === this._result?.col && r?.row === this._result?.row)) { + while (found && !this._cachedResults.find(r => r?.col === this._result?.col && r?.row === this._result?.row)) { if (this._result) { - results.push(this._result); + this._cachedResults.push(this._result); } found = this.findNext(term, searchOptions); } - for (const result of results) { + for (const result of this._cachedResults) { if (result) { const resultDecoration = this._showResultDecoration(result); if (resultDecoration) { @@ -108,7 +113,7 @@ export class SearchAddon implements ITerminalAddon { } } } - if (results.length > 0) { + if (this._cachedResults.length > 0) { this._cachedSearchTerm = term; } return true; @@ -517,6 +522,10 @@ export class SearchAddon implements ITerminalAddon { return false; } terminal.select(result.col, result.row, result.size); + const marker = terminal.registerMarker(undefined, result.row); + if (marker) { + terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'yellow' } }); + } // If it is not in the viewport then we scroll else it just gets selected if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { let scroll = result.row - terminal.buffer.active.viewportY; @@ -537,9 +546,10 @@ export class SearchAddon implements ITerminalAddon { if (!marker) { return undefined; } - const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'yellow' } }); + terminal.options.overviewRulerWidth = 10; + const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'blue' } }); findResultDecoration?.onRender((e) => { - if (!e.classList.contains('xterm-find-result-decoration') && result.term.length && e.clientWidth > 0) { + if (!e.classList.contains('xterm-find-result-decoration') && result.term.length && e.clientWidth > 0 && !e.classList.contains('xterm-decoration-overview-ruler')) { e.classList.add('xterm-find-result-decoration'); // decoration's clientWidth = actualCellWidth e.style.left = `${e.clientWidth * result.col}px`; diff --git a/css/xterm.css b/css/xterm.css index 9b9c47f80b..3160a26e71 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -180,7 +180,7 @@ } .xterm-find-result-decoration { - background-color: yellow; + background-color: blue; opacity: 60%; } .xterm-decoration-overview-ruler { From 26f8fce5387c68d446d145f0e6cb789fd7eedd43 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 16 Mar 2022 17:23:33 -0400 Subject: [PATCH 12/55] update color when select next happens --- addons/xterm-addon-search/src/SearchAddon.ts | 25 ++++++++++---------- css/xterm.css | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 1e54ee3bd6..67e1f80032 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -44,7 +44,7 @@ export class SearchAddon implements ITerminalAddon { private _result: ISearchResult | undefined; private _reset: boolean = false; private _cachedSearchTerm: string | undefined; - private _cachedResults: ISearchResult[] = []; + private _selectedDecoration: IDecoration | undefined; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -82,11 +82,9 @@ export class SearchAddon implements ITerminalAddon { } if (!this._reset && term === this._cachedSearchTerm) { - // this._resultDecorations.forEach(d => d.dispose()); - // this._resultDecorations = []; - // for (const decoration of this._cachedResults) { - // this._showResultDecoration(decoration); - // } + for (const decoration of this._resultDecorations) { + decoration.overviewRulerOptions = { color: 'orange' }; + } return this.findNext(term, searchOptions); } this._reset = false; @@ -94,18 +92,18 @@ export class SearchAddon implements ITerminalAddon { // new search, clear out the old decorations this._resultDecorations.forEach(d => d.dispose()); - this._resultDecorations = []; + const results: ISearchResult[] = []; searchOptions = searchOptions || {}; searchOptions.incremental = false; let found = this.findNext(term, searchOptions); - while (found && !this._cachedResults.find(r => r?.col === this._result?.col && r?.row === this._result?.row)) { + while (found && !results.find(r => r?.col === this._result?.col && r?.row === this._result?.row)) { if (this._result) { - this._cachedResults.push(this._result); + results.push(this._result); } found = this.findNext(term, searchOptions); } - for (const result of this._cachedResults) { + for (const result of results) { if (result) { const resultDecoration = this._showResultDecoration(result); if (resultDecoration) { @@ -113,7 +111,7 @@ export class SearchAddon implements ITerminalAddon { } } } - if (this._cachedResults.length > 0) { + if (results.length > 0) { this._cachedSearchTerm = term; } return true; @@ -517,6 +515,7 @@ export class SearchAddon implements ITerminalAddon { */ private _selectResult(result: ISearchResult | undefined): boolean { const terminal = this._terminal!; + this._selectedDecoration?.dispose(); if (!result) { terminal.clearSelection(); return false; @@ -524,7 +523,7 @@ export class SearchAddon implements ITerminalAddon { terminal.select(result.col, result.row, result.size); const marker = terminal.registerMarker(undefined, result.row); if (marker) { - terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'yellow' } }); + this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'blue' } }); } // If it is not in the viewport then we scroll else it just gets selected if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { @@ -547,7 +546,7 @@ export class SearchAddon implements ITerminalAddon { return undefined; } terminal.options.overviewRulerWidth = 10; - const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'blue' } }); + const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'orange' } }); findResultDecoration?.onRender((e) => { if (!e.classList.contains('xterm-find-result-decoration') && result.term.length && e.clientWidth > 0 && !e.classList.contains('xterm-decoration-overview-ruler')) { e.classList.add('xterm-find-result-decoration'); diff --git a/css/xterm.css b/css/xterm.css index 3160a26e71..f8f85f9e29 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -180,7 +180,7 @@ } .xterm-find-result-decoration { - background-color: blue; + background-color: grey; opacity: 60%; } .xterm-decoration-overview-ruler { From 90f64ba6ccd45378cbb91c87af7cd216e3e03029 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 17 Mar 2022 09:52:41 -0400 Subject: [PATCH 13/55] get it to work in the demo --- addons/xterm-addon-search/src/SearchAddon.ts | 6 +++++- src/browser/Decorations/BufferDecorationRenderer.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 67e1f80032..8027cd73ab 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -92,6 +92,7 @@ export class SearchAddon implements ITerminalAddon { // new search, clear out the old decorations this._resultDecorations.forEach(d => d.dispose()); + console.log('clearing'); const results: ISearchResult[] = []; searchOptions = searchOptions || {}; searchOptions.incremental = false; @@ -102,7 +103,7 @@ export class SearchAddon implements ITerminalAddon { } found = this.findNext(term, searchOptions); } - + console.log(results); for (const result of results) { if (result) { const resultDecoration = this._showResultDecoration(result); @@ -541,7 +542,10 @@ export class SearchAddon implements ITerminalAddon { */ private _showResultDecoration(result: ISearchResult): IDecoration | undefined { const terminal = this._terminal!; + // for demo to work + // const marker = terminal.registerMarker(undefined, result.row); const marker = terminal.registerMarker(undefined, result.row); + console.log(result.row, marker?.line); if (!marker) { return undefined; } diff --git a/src/browser/Decorations/BufferDecorationRenderer.ts b/src/browser/Decorations/BufferDecorationRenderer.ts index 116c09a58a..22dc73e9b0 100644 --- a/src/browser/Decorations/BufferDecorationRenderer.ts +++ b/src/browser/Decorations/BufferDecorationRenderer.ts @@ -97,7 +97,7 @@ export class BufferDecorationRenderer extends Disposable { private _refreshStyle(decoration: IInternalDecoration, element: HTMLElement): void { const line = decoration.marker.line - this._bufferService.buffers.active.ydisp; - if (line < 0 || line > this._bufferService.rows) { + if (line < 0 || line >= this._bufferService.rows) { // outside of viewport element.style.display = 'none'; } else { From c2145d5125cb794cd60b4cec6cd4b33cd585479e Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 17 Mar 2022 10:38:44 -0400 Subject: [PATCH 14/55] on dispose, clear the canvas at that position --- addons/xterm-addon-search/src/SearchAddon.ts | 17 +++++++++-------- .../Decorations/OverviewRulerRenderer.ts | 16 ++++++++++------ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 8027cd73ab..05c6892d57 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -62,6 +62,14 @@ export class SearchAddon implements ITerminalAddon { public dispose(): void { } + public clear(): void { + this._terminal?.clearSelection(); + this._resultDecorations.forEach(d => d.dispose()); + this._selectedDecoration?.dispose(); + this._resultDecorations = []; + this._reset = true; + } + /** * Find all instances of the term, selecting the next one with each * enter. If it doesn't exist, do nothing. @@ -75,9 +83,7 @@ export class SearchAddon implements ITerminalAddon { } if (!term || term.length === 0) { - this._terminal.clearSelection(); - this._resultDecorations.forEach(d => d.dispose()); - this._resultDecorations = []; + this.clear(); return false; } @@ -92,7 +98,6 @@ export class SearchAddon implements ITerminalAddon { // new search, clear out the old decorations this._resultDecorations.forEach(d => d.dispose()); - console.log('clearing'); const results: ISearchResult[] = []; searchOptions = searchOptions || {}; searchOptions.incremental = false; @@ -103,7 +108,6 @@ export class SearchAddon implements ITerminalAddon { } found = this.findNext(term, searchOptions); } - console.log(results); for (const result of results) { if (result) { const resultDecoration = this._showResultDecoration(result); @@ -542,10 +546,7 @@ export class SearchAddon implements ITerminalAddon { */ private _showResultDecoration(result: ISearchResult): IDecoration | undefined { const terminal = this._terminal!; - // for demo to work - // const marker = terminal.registerMarker(undefined, result.row); const marker = terminal.registerMarker(undefined, result.row); - console.log(result.row, marker?.line); if (!marker) { return undefined; } diff --git a/src/browser/Decorations/OverviewRulerRenderer.ts b/src/browser/Decorations/OverviewRulerRenderer.ts index 620e4dbbb2..824f27ef62 100644 --- a/src/browser/Decorations/OverviewRulerRenderer.ts +++ b/src/browser/Decorations/OverviewRulerRenderer.ts @@ -67,12 +67,7 @@ export class OverviewRulerRenderer extends Disposable { public override dispose(): void { for (const decoration of this._decorationElements) { - this._ctx?.clearRect( - 0, - Math.round(this._canvas.height * (decoration[0].marker.line / this._bufferService.buffers.active.lines.length)), - this._canvas.width, - window.devicePixelRatio - ); + decoration[0].dispose(); } this._decorationElements.clear(); this._canvas?.remove(); @@ -119,6 +114,15 @@ export class OverviewRulerRenderer extends Disposable { const element = this._decorationElements.get(decoration); if (!element) { this._decorationElements.set(decoration, this._canvas); + decoration.onDispose(() => { + this._ctx?.clearRect( + !decoration!.options!.overviewRulerOptions?.position || decoration!.options!.overviewRulerOptions?.position === 'left' ? 0 : decoration!.options!.overviewRulerOptions?.position === 'right' ? renderSizes[SizeIndex.OUTER_SIZE] + renderSizes[SizeIndex.INNER_SIZE]: renderSizes[SizeIndex.OUTER_SIZE], + Math.round(this._canvas.height * (decoration!.options!.marker.line / this._bufferService.buffers.active.lines.length)), + !decoration!.options!.overviewRulerOptions?.position ? this._width : decoration!.options!.overviewRulerOptions?.position === 'center' ? renderSizes[SizeIndex.INNER_SIZE] : renderSizes[SizeIndex.OUTER_SIZE], + // when a position is provided, the element has less width, so increase its height + window.devicePixelRatio * (decoration!.options!.overviewRulerOptions?.position ? 6 : 2) + ); + }); } this._refreshStyle(decoration, updateAnchor); } From de56d04ae51bca703f863b3f426d9972ee752288 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 17 Mar 2022 11:08:40 -0400 Subject: [PATCH 15/55] return decoration from show result --- addons/xterm-addon-search/src/SearchAddon.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 05c6892d57..164308965b 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -528,7 +528,7 @@ export class SearchAddon implements ITerminalAddon { terminal.select(result.col, result.row, result.size); const marker = terminal.registerMarker(undefined, result.row); if (marker) { - this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'blue' } }); + this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'blue', position: 'center' } }); } // If it is not in the viewport then we scroll else it just gets selected if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { @@ -550,10 +550,11 @@ export class SearchAddon implements ITerminalAddon { if (!marker) { return undefined; } + // TODO: remove/move? terminal.options.overviewRulerWidth = 10; - const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'orange' } }); + const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'orange', position: 'center' } }); findResultDecoration?.onRender((e) => { - if (!e.classList.contains('xterm-find-result-decoration') && result.term.length && e.clientWidth > 0 && !e.classList.contains('xterm-decoration-overview-ruler')) { + if (!e.classList.contains('xterm-find-result-decoration') && result.term.length && e.clientWidth > 0) { e.classList.add('xterm-find-result-decoration'); // decoration's clientWidth = actualCellWidth e.style.left = `${e.clientWidth * result.col}px`; From 9bc5090bac5bb4209b14ebba6a2099ecd275cc2a Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 11:47:26 -0400 Subject: [PATCH 16/55] use a map --- addons/xterm-addon-search/src/SearchAddon.ts | 42 +++++++++++--------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 164308965b..9a3bfb4a3a 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -40,9 +40,9 @@ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs export class SearchAddon implements ITerminalAddon { private _terminal: Terminal | undefined; - private _resultDecorations: IDecoration[] = []; + private _resultDecorations: Map = new Map(); private _result: ISearchResult | undefined; - private _reset: boolean = false; + private _dataChanged: boolean = false; private _cachedSearchTerm: string | undefined; private _selectedDecoration: IDecoration | undefined; /** @@ -57,17 +57,18 @@ export class SearchAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - this._terminal.onData(() => this._reset = true); + // TODO: should this be using on buffer changed instead? + this._terminal.onData(() => this._dataChanged = true); } public dispose(): void { } public clear(): void { this._terminal?.clearSelection(); - this._resultDecorations.forEach(d => d.dispose()); - this._selectedDecoration?.dispose(); - this._resultDecorations = []; - this._reset = true; + this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); + this._resultDecorations.clear(); + this._cachedSearchTerm = undefined; + this._dataChanged = true; } /** @@ -86,18 +87,18 @@ export class SearchAddon implements ITerminalAddon { this.clear(); return false; } - - if (!this._reset && term === this._cachedSearchTerm) { - for (const decoration of this._resultDecorations) { - decoration.overviewRulerOptions = { color: 'orange' }; - } + if (!this._dataChanged && term === this._cachedSearchTerm) { return this.findNext(term, searchOptions); } - this._reset = false; - + if (this._dataChanged && term === this._cachedSearchTerm) { + // TODO: + // add to decorations instead of starting from scratch + // by looking at resultDecoration.keys()[resultDecoration.length] + // to ybase + } // new search, clear out the old decorations - this._resultDecorations.forEach(d => d.dispose()); + this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); const results: ISearchResult[] = []; searchOptions = searchOptions || {}; searchOptions.incremental = false; @@ -112,10 +113,15 @@ export class SearchAddon implements ITerminalAddon { if (result) { const resultDecoration = this._showResultDecoration(result); if (resultDecoration) { - this._resultDecorations.push(resultDecoration); + const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; + decorationsForLine.push(resultDecoration); + this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine); } } } + if (this._dataChanged) { + this._dataChanged = false; + } if (results.length > 0) { this._cachedSearchTerm = term; } @@ -528,7 +534,7 @@ export class SearchAddon implements ITerminalAddon { terminal.select(result.col, result.row, result.size); const marker = terminal.registerMarker(undefined, result.row); if (marker) { - this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'blue', position: 'center' } }); + this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'blue' } }); } // If it is not in the viewport then we scroll else it just gets selected if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { @@ -552,7 +558,7 @@ export class SearchAddon implements ITerminalAddon { } // TODO: remove/move? terminal.options.overviewRulerWidth = 10; - const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'orange', position: 'center' } }); + const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color: 'orange', position: 'center' } }); findResultDecoration?.onRender((e) => { if (!e.classList.contains('xterm-find-result-decoration') && result.term.length && e.clientWidth > 0) { e.classList.add('xterm-find-result-decoration'); From a1468d0f589f5ca3e2b99f8c89de05af43bca25f Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 13:12:32 -0400 Subject: [PATCH 17/55] use start row and col --- addons/xterm-addon-search/src/SearchAddon.ts | 63 +++++++++++--------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 9a3bfb4a3a..b28e2bddb3 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -10,6 +10,8 @@ export interface ISearchOptions { wholeWord?: boolean; caseSensitive?: boolean; incremental?: boolean; + startRow?: number; + startCol?: number; } export interface ISearchPosition { @@ -40,11 +42,12 @@ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs export class SearchAddon implements ITerminalAddon { private _terminal: Terminal | undefined; - private _resultDecorations: Map = new Map(); private _result: ISearchResult | undefined; private _dataChanged: boolean = false; private _cachedSearchTerm: string | undefined; private _selectedDecoration: IDecoration | undefined; + private _resultDecorations: Map = new Map(); + private _searchResults: Map = new Map(); /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -65,6 +68,7 @@ export class SearchAddon implements ITerminalAddon { public clear(): void { this._terminal?.clearSelection(); + this._searchResults.clear(); this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); this._resultDecorations.clear(); this._cachedSearchTerm = undefined; @@ -87,42 +91,47 @@ export class SearchAddon implements ITerminalAddon { this.clear(); return false; } - if (!this._dataChanged && term === this._cachedSearchTerm) { - return this.findNext(term, searchOptions); - } - if (this._dataChanged && term === this._cachedSearchTerm) { - // TODO: - // add to decorations instead of starting from scratch - // by looking at resultDecoration.keys()[resultDecoration.length] - // to ybase + + if (term === this._cachedSearchTerm) { + if (!this._dataChanged) { + return this.findNext(term, searchOptions); + } + // set start row to avoid redoing work + searchOptions = searchOptions || {}; + const key = Array.from(this._searchResults.keys()).pop()?.split('-'); + if (key?.length === 2) { + searchOptions.startRow = Number.parseInt(key[0]) + 1; + searchOptions.startCol = Number.parseInt(key[1]); + console.log(searchOptions.startRow, searchOptions.startCol); + } + } else { + // new search, clear out the old decorations + this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); + this._resultDecorations.clear(); + this._searchResults.clear(); + searchOptions = searchOptions || {}; } - // new search, clear out the old decorations - this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); - const results: ISearchResult[] = []; - searchOptions = searchOptions || {}; searchOptions.incremental = false; let found = this.findNext(term, searchOptions); - while (found && !results.find(r => r?.col === this._result?.col && r?.row === this._result?.row)) { + while (found && (!this._result || !this._searchResults.get(`${this._result.row}-${this._result.col}`))) { if (this._result) { - results.push(this._result); + this._searchResults.set(`${this._result.row}-${this._result.col}`, this._result); } found = this.findNext(term, searchOptions); } - for (const result of results) { - if (result) { - const resultDecoration = this._showResultDecoration(result); - if (resultDecoration) { - const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; - decorationsForLine.push(resultDecoration); - this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine); - } + this._searchResults.forEach(result => { + const resultDecoration = this._showResultDecoration(result); + if (resultDecoration) { + const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; + decorationsForLine.push(resultDecoration); + this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine); } - } + }); if (this._dataChanged) { this._dataChanged = false; } - if (results.length > 0) { + if (this._searchResults.size > 0) { this._cachedSearchTerm = term; } return true; @@ -147,8 +156,8 @@ export class SearchAddon implements ITerminalAddon { return false; } - let startCol = 0; - let startRow = 0; + let startCol = searchOptions?.startCol || 0; + let startRow = searchOptions?.startRow || 0; let currentSelection: ISelectionPosition | undefined; if (this._terminal.hasSelection()) { const incremental = searchOptions ? searchOptions.incremental : false; From 6c4f8b42dd83f8e5ab279dc6bfa7bf2eaba013fb Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 13:29:59 -0400 Subject: [PATCH 18/55] append to results --- addons/xterm-addon-search/src/SearchAddon.ts | 9 +++------ typings/xterm.d.ts | 2 -- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index b28e2bddb3..3879241437 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -91,25 +91,22 @@ export class SearchAddon implements ITerminalAddon { this.clear(); return false; } - + searchOptions = searchOptions || {}; if (term === this._cachedSearchTerm) { if (!this._dataChanged) { return this.findNext(term, searchOptions); } - // set start row to avoid redoing work - searchOptions = searchOptions || {}; + // set start row and col to avoid redoing work const key = Array.from(this._searchResults.keys()).pop()?.split('-'); if (key?.length === 2) { searchOptions.startRow = Number.parseInt(key[0]) + 1; searchOptions.startCol = Number.parseInt(key[1]); - console.log(searchOptions.startRow, searchOptions.startCol); } } else { // new search, clear out the old decorations this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); this._resultDecorations.clear(); this._searchResults.clear(); - searchOptions = searchOptions || {}; } searchOptions.incremental = false; @@ -166,7 +163,7 @@ export class SearchAddon implements ITerminalAddon { currentSelection = this._terminal.getSelectionPosition()!; startRow = incremental ? currentSelection.startRow : currentSelection.endRow; startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn; - } else { + } else if (!startRow) { startRow = this._terminal.buffer.active.cursorY; startCol = this._terminal.buffer.active.cursorX; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d0149c198d..363cede3d5 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -7,8 +7,6 @@ * to be stable and consumed by external programs. */ -import { IModelDecorationOverviewRulerOptions } from 'vs/editor/common/model'; - /// declare module 'xterm' { From 47ec31361c547d952781afc0cbb591247a31ed9a Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 14:14:02 -0400 Subject: [PATCH 19/55] support colors --- addons/xterm-addon-search/src/SearchAddon.ts | 43 ++++++++++++-------- demo/client.ts | 5 ++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 3879241437..00e6830fc3 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -12,6 +12,8 @@ export interface ISearchOptions { incremental?: boolean; startRow?: number; startCol?: number; + overviewRulerResultDecorationColor?: string; + overviewRulerSelectionDecorationColor?: string; } export interface ISearchPosition { @@ -117,14 +119,17 @@ export class SearchAddon implements ITerminalAddon { } found = this.findNext(term, searchOptions); } - this._searchResults.forEach(result => { - const resultDecoration = this._showResultDecoration(result); - if (resultDecoration) { - const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; - decorationsForLine.push(resultDecoration); - this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine); - } - }); + if (searchOptions.overviewRulerResultDecorationColor && searchOptions.overviewRulerSelectionDecorationColor) { + this._searchResults.forEach(result => { + const resultDecoration = this._showResultDecoration(result, searchOptions!.overviewRulerResultDecorationColor!); + if (resultDecoration) { + const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; + decorationsForLine.push(resultDecoration); + this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine); + } + }); + } + if (this._dataChanged) { this._dataChanged = false; } @@ -211,7 +216,7 @@ export class SearchAddon implements ITerminalAddon { } // Set selection and scroll if a result was found - return this._selectResult(this._result); + return this._selectResult(this._result, searchOptions?.overviewRulerSelectionDecorationColor); } /** @@ -292,7 +297,7 @@ export class SearchAddon implements ITerminalAddon { if (!result && currentSelection) return true; // Set selection and scroll if a result was found - return this._selectResult(result); + return this._selectResult(result, searchOptions?.overviewRulerSelectionDecorationColor); } @@ -530,7 +535,7 @@ export class SearchAddon implements ITerminalAddon { * @param result The result to select. * @return Whethera result was selected. */ - private _selectResult(result: ISearchResult | undefined): boolean { + private _selectResult(result: ISearchResult | undefined, color?: string): boolean { const terminal = this._terminal!; this._selectedDecoration?.dispose(); if (!result) { @@ -538,10 +543,13 @@ export class SearchAddon implements ITerminalAddon { return false; } terminal.select(result.col, result.row, result.size); - const marker = terminal.registerMarker(undefined, result.row); - if (marker) { - this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: 'blue' } }); + if (color) { + const marker = terminal.registerMarker(undefined, result.row); + if (marker) { + this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color } }); + } } + // If it is not in the viewport then we scroll else it just gets selected if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { let scroll = result.row - terminal.buffer.active.viewportY; @@ -556,15 +564,14 @@ export class SearchAddon implements ITerminalAddon { * and @returns the decoration or undefined if * the marker has already been disposed of */ - private _showResultDecoration(result: ISearchResult): IDecoration | undefined { + private _showResultDecoration(result: ISearchResult, color: string): IDecoration | undefined { const terminal = this._terminal!; const marker = terminal.registerMarker(undefined, result.row); if (!marker) { return undefined; } - // TODO: remove/move? - terminal.options.overviewRulerWidth = 10; - const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color: 'orange', position: 'center' } }); + + const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color, position: 'center' } }); findResultDecoration?.onRender((e) => { if (!e.classList.contains('xterm-find-result-decoration') && result.term.length && e.clientWidth > 0) { e.classList.add('xterm-find-result-decoration'); diff --git a/demo/client.ts b/demo/client.ts index 692ad8e546..404435c413 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -108,7 +108,9 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions { regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked, - incremental: e.key !== `Enter` + incremental: e.key !== `Enter`, + overviewRulerResultDecorationColor: '#555753', + overviewRulerSelectionDecorationColor: '#ef2929' }; } @@ -202,6 +204,7 @@ function createTerminal(): void { addDomListener(paddingElement, 'change', setPadding); addDomListener(actionElements.find, 'keyup', (e) => { + term.options.overviewRulerWidth = 10; addons.search.instance.find(actionElements.find.value, getSearchOptions(e)); }); From 01fd14867327ba241de363a1beff956eb7a1e9df Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 14:46:49 -0400 Subject: [PATCH 20/55] clean up --- addons/xterm-addon-search/src/SearchAddon.ts | 6 +++--- css/xterm.css | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 00e6830fc3..32441f1cee 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -62,7 +62,6 @@ export class SearchAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - // TODO: should this be using on buffer changed instead? this._terminal.onData(() => this._dataChanged = true); } @@ -578,8 +577,9 @@ export class SearchAddon implements ITerminalAddon { // decoration's clientWidth = actualCellWidth e.style.left = `${e.clientWidth * result.col}px`; e.style.width = `${e.clientWidth * result.term.length}px`; - } - }); + e.style.backgroundColor = color; + e.style.color = color; + }}); return findResultDecoration; } } diff --git a/css/xterm.css b/css/xterm.css index f8f85f9e29..aa58915c0e 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -180,9 +180,9 @@ } .xterm-find-result-decoration { - background-color: grey; opacity: 60%; } + .xterm-decoration-overview-ruler { z-index: 7; position: absolute; From 8e68c67a0c57d2e0a12cbb5078c8095e1b353689 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 15:07:33 -0400 Subject: [PATCH 21/55] applyStyles --- addons/xterm-addon-search/src/SearchAddon.ts | 25 +++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 32441f1cee..8a8acf0691 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -546,6 +546,7 @@ export class SearchAddon implements ITerminalAddon { const marker = terminal.registerMarker(undefined, result.row); if (marker) { this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color } }); + this._selectedDecoration?.onRender((e) => this._applyStyles(e, color, result)); } } @@ -558,6 +559,20 @@ export class SearchAddon implements ITerminalAddon { return true; } + private _applyStyles(element: HTMLElement, color: string, result: ISearchResult): void { + if (element.clientWidth <= 0) { + return; + } + if (!element.classList.contains('xterm-find-result-decoration')) { + element.classList.add('xterm-find-result-decoration'); + // decoration's clientWidth = actualCellWidth + element.style.left = `${element.clientWidth * result.col}px`; + element.style.width = `${element.clientWidth * result.term.length}px`; + element.style.backgroundColor = color; + element.style.color = color; + } + } + /** * Registers a decoration for the @param result * and @returns the decoration or undefined if @@ -571,15 +586,7 @@ export class SearchAddon implements ITerminalAddon { } const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color, position: 'center' } }); - findResultDecoration?.onRender((e) => { - if (!e.classList.contains('xterm-find-result-decoration') && result.term.length && e.clientWidth > 0) { - e.classList.add('xterm-find-result-decoration'); - // decoration's clientWidth = actualCellWidth - e.style.left = `${e.clientWidth * result.col}px`; - e.style.width = `${e.clientWidth * result.term.length}px`; - e.style.backgroundColor = color; - e.style.color = color; - }}); + findResultDecoration?.onRender((e) => this._applyStyles(e, color, result)); return findResultDecoration; } } From 66dcd1694c915bcf56e08b354d0037407b0e4194 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 15:13:49 -0400 Subject: [PATCH 22/55] clean up jsdoc --- addons/xterm-addon-search/src/SearchAddon.ts | 21 +++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 8a8acf0691..d6ec1bb7f5 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -120,7 +120,7 @@ export class SearchAddon implements ITerminalAddon { } if (searchOptions.overviewRulerResultDecorationColor && searchOptions.overviewRulerSelectionDecorationColor) { this._searchResults.forEach(result => { - const resultDecoration = this._showResultDecoration(result, searchOptions!.overviewRulerResultDecorationColor!); + const resultDecoration = this._createResultDecoration(result, searchOptions!.overviewRulerResultDecorationColor!); if (resultDecoration) { const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; decorationsForLine.push(resultDecoration); @@ -532,7 +532,7 @@ export class SearchAddon implements ITerminalAddon { /** * Selects and scrolls to a result. * @param result The result to select. - * @return Whethera result was selected. + * @return Whether a result was selected. */ private _selectResult(result: ISearchResult | undefined, color?: string): boolean { const terminal = this._terminal!; @@ -559,6 +559,13 @@ export class SearchAddon implements ITerminalAddon { return true; } + /** + * Applies styles to the decoration when it is rendered + * @param element the decoration's element + * @param color the color to apply + * @param result the search result associated with the decoration + * @returns + */ private _applyStyles(element: HTMLElement, color: string, result: ISearchResult): void { if (element.clientWidth <= 0) { return; @@ -574,17 +581,17 @@ export class SearchAddon implements ITerminalAddon { } /** - * Registers a decoration for the @param result - * and @returns the decoration or undefined if - * the marker has already been disposed of + * Creates a decoration for the result and applies styles + * @param result the search result for which to create the decoration + * @param color the color to use for the decoration + * @returns the {@link IDecoration} or undefined if the marker has already been disposed of */ - private _showResultDecoration(result: ISearchResult, color: string): IDecoration | undefined { + private _createResultDecoration(result: ISearchResult, color: string): IDecoration | undefined { const terminal = this._terminal!; const marker = terminal.registerMarker(undefined, result.row); if (!marker) { return undefined; } - const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color, position: 'center' } }); findResultDecoration?.onRender((e) => this._applyStyles(e, color, result)); return findResultDecoration; From 15c2432b6fb20594806c74d900437169012acd44 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 15:23:49 -0400 Subject: [PATCH 23/55] jsdoc --- typings/xterm.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 363cede3d5..451da2e8ea 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -954,6 +954,7 @@ declare module 'xterm' { * (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the * alt buffer is active, undefined is returned. * @param cursorYOffset The y position offset of the marker from the cursor. + * @param row The y position of the marker. * @returns The new marker or undefined. */ registerMarker(cursorYOffset?: number, row?: number): IMarker | undefined; From 85683a457d119bc156ca1b8543d457e1f3324896 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 15:27:50 -0400 Subject: [PATCH 24/55] add to col, not row --- addons/xterm-addon-search/src/SearchAddon.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index d6ec1bb7f5..201c74204a 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -100,8 +100,8 @@ export class SearchAddon implements ITerminalAddon { // set start row and col to avoid redoing work const key = Array.from(this._searchResults.keys()).pop()?.split('-'); if (key?.length === 2) { - searchOptions.startRow = Number.parseInt(key[0]) + 1; - searchOptions.startCol = Number.parseInt(key[1]); + searchOptions.startRow = Number.parseInt(key[0]); + searchOptions.startCol = Number.parseInt(key[1]) + 1; } } else { // new search, clear out the old decorations From dd503785bef8d68dc9450f68a7f6da36b27f6cc9 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 18 Mar 2022 15:28:16 -0400 Subject: [PATCH 25/55] Update css/xterm.css Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- css/xterm.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/xterm.css b/css/xterm.css index aa58915c0e..b9382ccc72 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -180,7 +180,7 @@ } .xterm-find-result-decoration { - opacity: 60%; + opacity: 0.6; } .xterm-decoration-overview-ruler { From 8328fcac52ec64320a57a41defd93a4d5634e037 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 15:32:24 -0400 Subject: [PATCH 26/55] track disposable --- addons/xterm-addon-search/src/SearchAddon.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 201c74204a..f0d64eb924 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -50,6 +50,7 @@ export class SearchAddon implements ITerminalAddon { private _selectedDecoration: IDecoration | undefined; private _resultDecorations: Map = new Map(); private _searchResults: Map = new Map(); + private _onDataDisposable: IDisposable | undefined; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -62,10 +63,13 @@ export class SearchAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - this._terminal.onData(() => this._dataChanged = true); + this._onDataDisposable = this._terminal.onData(() => this._dataChanged = true); } - public dispose(): void { } + public dispose(): void { + this.clear(); + this._onDataDisposable?.dispose(); + } public clear(): void { this._terminal?.clearSelection(); @@ -576,7 +580,6 @@ export class SearchAddon implements ITerminalAddon { element.style.left = `${element.clientWidth * result.col}px`; element.style.width = `${element.clientWidth * result.term.length}px`; element.style.backgroundColor = color; - element.style.color = color; } } From fef30bdba457b03e2d0f7c204b1e127449086dba Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 15:38:31 -0400 Subject: [PATCH 27/55] revert row register marker changes --- addons/xterm-addon-search/src/SearchAddon.ts | 4 ++-- src/browser/Terminal.ts | 5 +---- src/browser/Types.d.ts | 2 +- src/browser/public/Terminal.ts | 7 ++----- typings/xterm.d.ts | 3 +-- 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index f0d64eb924..90749e9206 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -547,7 +547,7 @@ export class SearchAddon implements ITerminalAddon { } terminal.select(result.col, result.row, result.size); if (color) { - const marker = terminal.registerMarker(undefined, result.row); + const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); if (marker) { this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color } }); this._selectedDecoration?.onRender((e) => this._applyStyles(e, color, result)); @@ -591,7 +591,7 @@ export class SearchAddon implements ITerminalAddon { */ private _createResultDecoration(result: ISearchResult, color: string): IDecoration | undefined { const terminal = this._terminal!; - const marker = terminal.registerMarker(undefined, result.row); + const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); if (!marker) { return undefined; } diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 496b8dfa6a..5803e8c473 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -1012,14 +1012,11 @@ export class Terminal extends CoreTerminal implements ITerminal { return this.buffer.markers; } - public addMarker(cursorYOffset: number, row?: number): IMarker | undefined { + public addMarker(cursorYOffset: number): IMarker | undefined { // Disallow markers on the alt buffer if (this.buffer !== this.buffers.normal) { return; } - if (row) { - return this.buffer.addMarker(row); - } return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset); } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 6e98eff6af..8860bb4160 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -61,7 +61,7 @@ export interface IPublicTerminal extends IDisposable { registerLinkProvider(linkProvider: ILinkProvider): IDisposable; registerCharacterJoiner(handler: (text: string) => [number, number][]): number; deregisterCharacterJoiner(joinerId: number): void; - addMarker(cursorYOffset: number, col?: number, row?: number): IMarker | undefined; + addMarker(cursorYOffset: number): IMarker | undefined; registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; hasSelection(): boolean; getSelection(): string; diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 8c290fda0e..1acde934d8 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -166,13 +166,10 @@ export class Terminal implements ITerminalApi { this._checkProposedApi(); this._core.deregisterCharacterJoiner(joinerId); } - public registerMarker(cursorYOffset: number = 0, row?: number): IMarker | undefined { + public registerMarker(cursorYOffset: number = 0): IMarker | undefined { this._checkProposedApi(); this._verifyIntegers(cursorYOffset); - if (row) { - this._verifyPositiveIntegers(row); - } - return this._core.addMarker(cursorYOffset, row); + return this._core.addMarker(cursorYOffset); } public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { this._checkProposedApi(); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 451da2e8ea..d1eb38903a 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -954,10 +954,9 @@ declare module 'xterm' { * (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the * alt buffer is active, undefined is returned. * @param cursorYOffset The y position offset of the marker from the cursor. - * @param row The y position of the marker. * @returns The new marker or undefined. */ - registerMarker(cursorYOffset?: number, row?: number): IMarker | undefined; + registerMarker(cursorYOffset?: number): IMarker | undefined; /** * @deprecated use `registerMarker` instead. From 61e5c10fd30b73f73f9ebb6714582e4f26dd0907 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 19:07:39 -0400 Subject: [PATCH 28/55] pull apart and refactor findNext --- addons/xterm-addon-search/src/SearchAddon.ts | 98 ++++++++++---------- demo/client.ts | 8 +- demo/index.html | 2 +- typings/xterm.d.ts | 12 +++ 4 files changed, 64 insertions(+), 56 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 90749e9206..f592b78e26 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -10,10 +10,9 @@ export interface ISearchOptions { wholeWord?: boolean; caseSensitive?: boolean; incremental?: boolean; + highlightAllMatches?: boolean; startRow?: number; startCol?: number; - overviewRulerResultDecorationColor?: string; - overviewRulerSelectionDecorationColor?: string; } export interface ISearchPosition { @@ -72,6 +71,7 @@ export class SearchAddon implements ITerminalAddon { } public clear(): void { + this._selectedDecoration?.dispose(); this._terminal?.clearSelection(); this._searchResults.clear(); this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); @@ -81,17 +81,23 @@ export class SearchAddon implements ITerminalAddon { } /** - * Find all instances of the term, selecting the next one with each - * enter. If it doesn't exist, do nothing. + * Find the next instance of the term, then scroll to and select it. If it + * doesn't exist, do nothing. * @param term The search term. * @param searchOptions Search options. * @return Whether a result was found. */ - public find(term: string, searchOptions?: ISearchOptions): boolean { + public findNext(term: string, searchOptions?: ISearchOptions): boolean { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } + return searchOptions?.highlightAllMatches ? this._highlightAllMatches(term, searchOptions) : this._findAndSelectNext(term, searchOptions); + } + private _highlightAllMatches(term: string, searchOptions: ISearchOptions): boolean { + if (!this._terminal) { + throw new Error('cannot find all matches with no terminal'); + } if (!term || term.length === 0) { this.clear(); return false; @@ -99,7 +105,7 @@ export class SearchAddon implements ITerminalAddon { searchOptions = searchOptions || {}; if (term === this._cachedSearchTerm) { if (!this._dataChanged) { - return this.findNext(term, searchOptions); + return this._findAndSelectNext(term, searchOptions); } // set start row and col to avoid redoing work const key = Array.from(this._searchResults.keys()).pop()?.split('-'); @@ -113,51 +119,45 @@ export class SearchAddon implements ITerminalAddon { this._resultDecorations.clear(); this._searchResults.clear(); } - + if (!this._terminal.options.overviewRulerWidth) { + this._terminal.options.overviewRulerWidth = 10; + } + if (!this._terminal.options.findResultDecorationColor) { + this._terminal.options.findResultDecorationColor = '#555753'; + } + if (!this._terminal.options.findResultSelectedDecorationColor) { + this._terminal.options.findResultSelectedDecorationColor = '#ef2929'; + } searchOptions.incremental = false; - let found = this.findNext(term, searchOptions); + let found = this._findAndSelectNext(term, searchOptions); while (found && (!this._result || !this._searchResults.get(`${this._result.row}-${this._result.col}`))) { if (this._result) { this._searchResults.set(`${this._result.row}-${this._result.col}`, this._result); } - found = this.findNext(term, searchOptions); - } - if (searchOptions.overviewRulerResultDecorationColor && searchOptions.overviewRulerSelectionDecorationColor) { - this._searchResults.forEach(result => { - const resultDecoration = this._createResultDecoration(result, searchOptions!.overviewRulerResultDecorationColor!); - if (resultDecoration) { - const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; - decorationsForLine.push(resultDecoration); - this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine); - } - }); - } - + found = this._findAndSelectNext(term, searchOptions); + } + this._searchResults.forEach(result => { + const resultDecoration = this._createResultDecoration(result); + if (resultDecoration) { + const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; + decorationsForLine.push(resultDecoration); + this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine); + } + }); if (this._dataChanged) { this._dataChanged = false; } if (this._searchResults.size > 0) { this._cachedSearchTerm = term; } - return true; + return this._searchResults.size > 0; } - - /** - * Find the next instance of the term, then scroll to and select it. If it - * doesn't exist, do nothing. - * @param term The search term. - * @param searchOptions Search options. - * @return Whether a result was found. - */ - public findNext(term: string, searchOptions?: ISearchOptions): boolean { - if (!this._terminal) { - throw new Error('Cannot use addon until it has been loaded'); - } - - if (!term || term.length === 0) { + private _findAndSelectNext(term: string, searchOptions?: ISearchOptions): boolean { + if (!this._terminal || !term || term.length === 0) { this._result = undefined; - this._terminal.clearSelection(); + this._terminal?.clearSelection(); + this.clear(); return false; } @@ -219,9 +219,8 @@ export class SearchAddon implements ITerminalAddon { } // Set selection and scroll if a result was found - return this._selectResult(this._result, searchOptions?.overviewRulerSelectionDecorationColor); + return this._selectResult(this._result, searchOptions?.highlightAllMatches); } - /** * Find the previous instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. @@ -300,7 +299,7 @@ export class SearchAddon implements ITerminalAddon { if (!result && currentSelection) return true; // Set selection and scroll if a result was found - return this._selectResult(result, searchOptions?.overviewRulerSelectionDecorationColor); + return this._selectResult(result, searchOptions?.highlightAllMatches); } @@ -538,7 +537,7 @@ export class SearchAddon implements ITerminalAddon { * @param result The result to select. * @return Whether a result was selected. */ - private _selectResult(result: ISearchResult | undefined, color?: string): boolean { + private _selectResult(result: ISearchResult | undefined, highlightAllMatches?: boolean): boolean { const terminal = this._terminal!; this._selectedDecoration?.dispose(); if (!result) { @@ -546,11 +545,11 @@ export class SearchAddon implements ITerminalAddon { return false; } terminal.select(result.col, result.row, result.size); - if (color) { + if (this._terminal?.options.findResultSelectedDecorationColor && highlightAllMatches) { const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); if (marker) { - this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color } }); - this._selectedDecoration?.onRender((e) => this._applyStyles(e, color, result)); + this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: this._terminal!.options.findResultSelectedDecorationColor } }); + this._selectedDecoration?.onRender((e) => this._applyStyles(e, this._terminal!.options.findResultSelectedDecorationColor!, result)); } } @@ -589,14 +588,17 @@ export class SearchAddon implements ITerminalAddon { * @param color the color to use for the decoration * @returns the {@link IDecoration} or undefined if the marker has already been disposed of */ - private _createResultDecoration(result: ISearchResult, color: string): IDecoration | undefined { + private _createResultDecoration(result: ISearchResult): IDecoration | undefined { const terminal = this._terminal!; const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); - if (!marker) { + if (!marker || !this._terminal?.options.findResultDecorationColor) { return undefined; } - const findResultDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color, position: 'center' } }); - findResultDecoration?.onRender((e) => this._applyStyles(e, color, result)); + const findResultDecoration = terminal.registerDecoration( + { marker, + overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color: this._terminal.options.findResultDecorationColor, position: 'center' } + }); + findResultDecoration?.onRender((e) => this._applyStyles(e, this._terminal!.options.findResultDecorationColor!, result)); return findResultDecoration; } } diff --git a/demo/client.ts b/demo/client.ts index 404435c413..98aa2e317d 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -109,8 +109,7 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions { wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked, incremental: e.key !== `Enter`, - overviewRulerResultDecorationColor: '#555753', - overviewRulerSelectionDecorationColor: '#ef2929' + highlightAllMatches: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked, }; } @@ -203,11 +202,6 @@ function createTerminal(): void { addDomListener(paddingElement, 'change', setPadding); - addDomListener(actionElements.find, 'keyup', (e) => { - term.options.overviewRulerWidth = 10; - addons.search.instance.find(actionElements.find.value, getSearchOptions(e)); - }); - addDomListener(actionElements.findNext, 'keyup', (e) => { addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e)); }); diff --git a/demo/index.html b/demo/index.html index 13748e9b43..b89726de6f 100644 --- a/demo/index.html +++ b/demo/index.html @@ -38,12 +38,12 @@

Addons

Addons Control

SearchAddon

- +

SerializeAddon

diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d1eb38903a..3513feb757 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -272,6 +272,18 @@ declare module 'xterm' { * ruler will be hidden when not set. */ overviewRulerWidth?: number; + + /** + * The color for all find result decorations + * in the overview ruler + */ + findResultDecorationColor?: string; + + /** + * The color for the currently selected decoration + * when all matches are displayed + */ + findResultSelectedDecorationColor?: string; } /** From e79c21d4b2c490f0d9a70263a91f20efe6fe5007 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 19:24:27 -0400 Subject: [PATCH 29/55] get it to work for previous too --- addons/xterm-addon-search/src/SearchAddon.ts | 53 +++++++++++--------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index f592b78e26..627c4a6313 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -91,10 +91,10 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - return searchOptions?.highlightAllMatches ? this._highlightAllMatches(term, searchOptions) : this._findAndSelectNext(term, searchOptions); + return searchOptions?.highlightAllMatches ? this._highlightAllMatches(term, searchOptions, 'next') : this._findAndSelectNext(term, searchOptions); } - private _highlightAllMatches(term: string, searchOptions: ISearchOptions): boolean { + private _highlightAllMatches(term: string, searchOptions: ISearchOptions, type: 'next' | 'previous'): boolean { if (!this._terminal) { throw new Error('cannot find all matches with no terminal'); } @@ -129,12 +129,12 @@ export class SearchAddon implements ITerminalAddon { this._terminal.options.findResultSelectedDecorationColor = '#ef2929'; } searchOptions.incremental = false; - let found = this._findAndSelectNext(term, searchOptions); + let found = type === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); while (found && (!this._result || !this._searchResults.get(`${this._result.row}-${this._result.col}`))) { if (this._result) { this._searchResults.set(`${this._result.row}-${this._result.col}`, this._result); } - found = this._findAndSelectNext(term, searchOptions); + found = type === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); } this._searchResults.forEach(result => { const resultDecoration = this._createResultDecoration(result); @@ -232,16 +232,24 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } + return searchOptions?.highlightAllMatches ? this._highlightAllMatches(term, searchOptions, 'previous') : this._findAndSelectPrevious(term, searchOptions); + } - if (!term || term.length === 0) { - this._terminal.clearSelection(); + private _findAndSelectPrevious(term: string, searchOptions?: ISearchOptions): boolean { + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } + + if (!this._terminal || !term || term.length === 0) { + this._result = undefined; + this._terminal?.clearSelection(); + this.clear(); return false; } const isReverseSearch = true; - let startRow = this._terminal.buffer.active.baseY + this._terminal.rows; - let startCol = this._terminal.cols; - let result: ISearchResult | undefined; + let startRow = searchOptions?.startRow || this._terminal.buffer.active.baseY + this._terminal.rows; + let startCol = searchOptions?.startCol || this._terminal.cols; const incremental = searchOptions ? searchOptions.incremental : false; let currentSelection: ISelectionPosition | undefined; if (this._terminal.hasSelection()) { @@ -259,50 +267,49 @@ export class SearchAddon implements ITerminalAddon { if (incremental) { // Try to expand selection to right first. - result = this._findInLine(term, searchPosition, searchOptions, false); - const isOldResultHighlighted = result && result.row === startRow && result.col === startCol; + this._result = this._findInLine(term, searchPosition, searchOptions, false); + const isOldResultHighlighted = this._result && this._result.row === startRow && this._result.col === startCol; if (!isOldResultHighlighted) { // If selection was not able to be expanded to the right, then try reverse search if (currentSelection) { searchPosition.startRow = currentSelection.endRow; searchPosition.startCol = currentSelection.endColumn; } - result = this._findInLine(term, searchPosition, searchOptions, true); + this._result = this._findInLine(term, searchPosition, searchOptions, true); } } else { - result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); + this._result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); } // Search from startRow - 1 to top - if (!result) { + if (!this._result) { searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols); for (let y = startRow - 1; y >= 0; y--) { searchPosition.startRow = y; - result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); - if (result) { + this._result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); + if (this._result) { break; } } } // If we hit the top and didn't search from the very bottom wrap back down - if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) { + if (!this._result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) { for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) { searchPosition.startRow = y; - result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); - if (result) { + this._result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); + if (this._result) { break; } } } // If there is only one result, return true. - if (!result && currentSelection) return true; + if (!this._result && currentSelection) return true; - // Set selection and scroll if a result was found - return this._selectResult(result, searchOptions?.highlightAllMatches); + // Set selection and scroll if a this._result was found + return this._selectResult(this._result, searchOptions?.highlightAllMatches); } - /** * Sets up a line cache with a ttl */ From a0aae62cb7f1344bed280bbe15602cc9f6fe7ec4 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 18 Mar 2022 19:43:00 -0400 Subject: [PATCH 30/55] get rid of start col end col --- addons/xterm-addon-search/src/SearchAddon.ts | 39 ++++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 627c4a6313..422f060bdf 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -11,8 +11,6 @@ export interface ISearchOptions { caseSensitive?: boolean; incremental?: boolean; highlightAllMatches?: boolean; - startRow?: number; - startCol?: number; } export interface ISearchPosition { @@ -50,6 +48,7 @@ export class SearchAddon implements ITerminalAddon { private _resultDecorations: Map = new Map(); private _searchResults: Map = new Map(); private _onDataDisposable: IDisposable | undefined; + private _addToPrior: boolean = false; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -107,12 +106,7 @@ export class SearchAddon implements ITerminalAddon { if (!this._dataChanged) { return this._findAndSelectNext(term, searchOptions); } - // set start row and col to avoid redoing work - const key = Array.from(this._searchResults.keys()).pop()?.split('-'); - if (key?.length === 2) { - searchOptions.startRow = Number.parseInt(key[0]); - searchOptions.startCol = Number.parseInt(key[1]) + 1; - } + this._addToPrior = true; } else { // new search, clear out the old decorations this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); @@ -147,6 +141,9 @@ export class SearchAddon implements ITerminalAddon { if (this._dataChanged) { this._dataChanged = false; } + if (this._addToPrior) { + this._addToPrior = false; + } if (this._searchResults.size > 0) { this._cachedSearchTerm = term; } @@ -161,8 +158,16 @@ export class SearchAddon implements ITerminalAddon { return false; } - let startCol = searchOptions?.startCol || 0; - let startRow = searchOptions?.startRow || 0; + let startCol = 0; + let startRow = 0; + if (searchOptions?.highlightAllMatches && this._addToPrior) { + // set start row and col to avoid redoing work + const key = Array.from(this._searchResults.keys()).pop()?.split('-'); + if (key?.length === 2) { + startRow = Number.parseInt(key[0]); + startCol = Number.parseInt(key[1]) + 1; + } + } let currentSelection: ISelectionPosition | undefined; if (this._terminal.hasSelection()) { const incremental = searchOptions ? searchOptions.incremental : false; @@ -247,9 +252,19 @@ export class SearchAddon implements ITerminalAddon { return false; } + let startRow = this._terminal.buffer.active.baseY + this._terminal.rows; + let startCol = this._terminal.cols; const isReverseSearch = true; - let startRow = searchOptions?.startRow || this._terminal.buffer.active.baseY + this._terminal.rows; - let startCol = searchOptions?.startCol || this._terminal.cols; + // if (searchOptions?.highlightAllMatches && this._addToPrior) { + // // set start row and col to avoid redoing work + // // TODO: fix this will mess with the order that they're iterated through + // const key = Array.from(this._searchResults.keys()).pop()?.split('-'); + // if (key?.length === 2) { + // startRow = Number.parseInt(key[0]); + // startCol = Number.parseInt(key[1]) + 1; + // } + // } + const incremental = searchOptions ? searchOptions.incremental : false; let currentSelection: ISelectionPosition | undefined; if (this._terminal.hasSelection()) { From 8225ecd3bad9b66df1e7341822c1a34c54bf8e96 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 10:24:46 -0400 Subject: [PATCH 31/55] refactor from highlightAllMatches -> decorations --- addons/xterm-addon-search/src/SearchAddon.ts | 41 ++++++++++---------- demo/client.ts | 2 +- typings/xterm.d.ts | 12 ------ 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 422f060bdf..f7cf517596 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -10,7 +10,12 @@ export interface ISearchOptions { wholeWord?: boolean; caseSensitive?: boolean; incremental?: boolean; - highlightAllMatches?: boolean; + decorations?: IDecorationColor; +} + +interface IDecorationColor { + matchColor: string; + selectedColor: string; } export interface ISearchPosition { @@ -90,7 +95,7 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - return searchOptions?.highlightAllMatches ? this._highlightAllMatches(term, searchOptions, 'next') : this._findAndSelectNext(term, searchOptions); + return searchOptions?.decorations ? this._highlightAllMatches(term, searchOptions, 'next') : this._findAndSelectNext(term, searchOptions); } private _highlightAllMatches(term: string, searchOptions: ISearchOptions, type: 'next' | 'previous'): boolean { @@ -116,12 +121,6 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal.options.overviewRulerWidth) { this._terminal.options.overviewRulerWidth = 10; } - if (!this._terminal.options.findResultDecorationColor) { - this._terminal.options.findResultDecorationColor = '#555753'; - } - if (!this._terminal.options.findResultSelectedDecorationColor) { - this._terminal.options.findResultSelectedDecorationColor = '#ef2929'; - } searchOptions.incremental = false; let found = type === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); while (found && (!this._result || !this._searchResults.get(`${this._result.row}-${this._result.col}`))) { @@ -131,7 +130,7 @@ export class SearchAddon implements ITerminalAddon { found = type === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); } this._searchResults.forEach(result => { - const resultDecoration = this._createResultDecoration(result); + const resultDecoration = this._createResultDecoration(result, searchOptions.decorations); if (resultDecoration) { const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; decorationsForLine.push(resultDecoration); @@ -160,7 +159,7 @@ export class SearchAddon implements ITerminalAddon { let startCol = 0; let startRow = 0; - if (searchOptions?.highlightAllMatches && this._addToPrior) { + if (searchOptions?.decorations && this._addToPrior) { // set start row and col to avoid redoing work const key = Array.from(this._searchResults.keys()).pop()?.split('-'); if (key?.length === 2) { @@ -224,7 +223,7 @@ export class SearchAddon implements ITerminalAddon { } // Set selection and scroll if a result was found - return this._selectResult(this._result, searchOptions?.highlightAllMatches); + return this._selectResult(this._result, searchOptions?.decorations); } /** * Find the previous instance of the term, then scroll to and select it. If it @@ -237,7 +236,7 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - return searchOptions?.highlightAllMatches ? this._highlightAllMatches(term, searchOptions, 'previous') : this._findAndSelectPrevious(term, searchOptions); + return searchOptions?.decorations ? this._highlightAllMatches(term, searchOptions, 'previous') : this._findAndSelectPrevious(term, searchOptions); } private _findAndSelectPrevious(term: string, searchOptions?: ISearchOptions): boolean { @@ -322,7 +321,7 @@ export class SearchAddon implements ITerminalAddon { if (!this._result && currentSelection) return true; // Set selection and scroll if a this._result was found - return this._selectResult(this._result, searchOptions?.highlightAllMatches); + return this._selectResult(this._result, searchOptions?.decorations); } /** @@ -559,7 +558,7 @@ export class SearchAddon implements ITerminalAddon { * @param result The result to select. * @return Whether a result was selected. */ - private _selectResult(result: ISearchResult | undefined, highlightAllMatches?: boolean): boolean { + private _selectResult(result: ISearchResult | undefined, decorations?: IDecorationColor): boolean { const terminal = this._terminal!; this._selectedDecoration?.dispose(); if (!result) { @@ -567,11 +566,11 @@ export class SearchAddon implements ITerminalAddon { return false; } terminal.select(result.col, result.row, result.size); - if (this._terminal?.options.findResultSelectedDecorationColor && highlightAllMatches) { + if (decorations?.selectedColor) { const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); if (marker) { - this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: this._terminal!.options.findResultSelectedDecorationColor } }); - this._selectedDecoration?.onRender((e) => this._applyStyles(e, this._terminal!.options.findResultSelectedDecorationColor!, result)); + this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: decorations.selectedColor } }); + this._selectedDecoration?.onRender((e) => this._applyStyles(e, decorations.selectedColor, result)); } } @@ -610,17 +609,17 @@ export class SearchAddon implements ITerminalAddon { * @param color the color to use for the decoration * @returns the {@link IDecoration} or undefined if the marker has already been disposed of */ - private _createResultDecoration(result: ISearchResult): IDecoration | undefined { + private _createResultDecoration(result: ISearchResult, decorations?: IDecorationColor): IDecoration | undefined { const terminal = this._terminal!; const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); - if (!marker || !this._terminal?.options.findResultDecorationColor) { + if (!marker || !decorations?.matchColor) { return undefined; } const findResultDecoration = terminal.registerDecoration( { marker, - overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color: this._terminal.options.findResultDecorationColor, position: 'center' } + overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color: decorations.matchColor, position: 'center' } }); - findResultDecoration?.onRender((e) => this._applyStyles(e, this._terminal!.options.findResultDecorationColor!, result)); + findResultDecoration?.onRender((e) => this._applyStyles(e, decorations.matchColor, result)); return findResultDecoration; } } diff --git a/demo/client.ts b/demo/client.ts index 98aa2e317d..faf3e0bef1 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -109,7 +109,7 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions { wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked, incremental: e.key !== `Enter`, - highlightAllMatches: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked, + decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? { matchColor: '#555753', selectedColor: '#ef2929' } : undefined }; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 3513feb757..d1eb38903a 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -272,18 +272,6 @@ declare module 'xterm' { * ruler will be hidden when not set. */ overviewRulerWidth?: number; - - /** - * The color for all find result decorations - * in the overview ruler - */ - findResultDecorationColor?: string; - - /** - * The color for the currently selected decoration - * when all matches are displayed - */ - findResultSelectedDecorationColor?: string; } /** From 12a999dfe051422006bcd6a69b85dc004d79b8fb Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 10:54:01 -0400 Subject: [PATCH 32/55] get rid of start row/col code --- addons/xterm-addon-search/src/SearchAddon.ts | 42 ++++---------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index f7cf517596..7ec081f5f3 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -53,7 +53,6 @@ export class SearchAddon implements ITerminalAddon { private _resultDecorations: Map = new Map(); private _searchResults: Map = new Map(); private _onDataDisposable: IDisposable | undefined; - private _addToPrior: boolean = false; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -107,17 +106,14 @@ export class SearchAddon implements ITerminalAddon { return false; } searchOptions = searchOptions || {}; - if (term === this._cachedSearchTerm) { - if (!this._dataChanged) { - return this._findAndSelectNext(term, searchOptions); - } - this._addToPrior = true; - } else { - // new search, clear out the old decorations - this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); - this._resultDecorations.clear(); - this._searchResults.clear(); + if (term === this._cachedSearchTerm && !this._dataChanged) { + return type === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); } + // new search, clear out the old decorations + this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); + this._resultDecorations.clear(); + this._searchResults.clear(); + if (!this._terminal.options.overviewRulerWidth) { this._terminal.options.overviewRulerWidth = 10; } @@ -137,12 +133,7 @@ export class SearchAddon implements ITerminalAddon { this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine); } }); - if (this._dataChanged) { - this._dataChanged = false; - } - if (this._addToPrior) { - this._addToPrior = false; - } + this._dataChanged = false; if (this._searchResults.size > 0) { this._cachedSearchTerm = term; } @@ -159,14 +150,6 @@ export class SearchAddon implements ITerminalAddon { let startCol = 0; let startRow = 0; - if (searchOptions?.decorations && this._addToPrior) { - // set start row and col to avoid redoing work - const key = Array.from(this._searchResults.keys()).pop()?.split('-'); - if (key?.length === 2) { - startRow = Number.parseInt(key[0]); - startCol = Number.parseInt(key[1]) + 1; - } - } let currentSelection: ISelectionPosition | undefined; if (this._terminal.hasSelection()) { const incremental = searchOptions ? searchOptions.incremental : false; @@ -254,15 +237,6 @@ export class SearchAddon implements ITerminalAddon { let startRow = this._terminal.buffer.active.baseY + this._terminal.rows; let startCol = this._terminal.cols; const isReverseSearch = true; - // if (searchOptions?.highlightAllMatches && this._addToPrior) { - // // set start row and col to avoid redoing work - // // TODO: fix this will mess with the order that they're iterated through - // const key = Array.from(this._searchResults.keys()).pop()?.split('-'); - // if (key?.length === 2) { - // startRow = Number.parseInt(key[0]); - // startCol = Number.parseInt(key[1]) + 1; - // } - // } const incremental = searchOptions ? searchOptions.incremental : false; let currentSelection: ISelectionPosition | undefined; From bec4cb37d4031dadbbeff495d0d41436d2d03e06 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 10:58:44 -0400 Subject: [PATCH 33/55] set data changed to false if true --- addons/xterm-addon-search/src/SearchAddon.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 7ec081f5f3..33e38e95e8 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -133,7 +133,9 @@ export class SearchAddon implements ITerminalAddon { this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine); } }); - this._dataChanged = false; + if (this._dataChanged) { + this._dataChanged = false; + } if (this._searchResults.size > 0) { this._cachedSearchTerm = term; } From 8ff34a6aee1e2f083056af15dc4c06822d3c7003 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 11:17:24 -0400 Subject: [PATCH 34/55] update decorations dynamically when the buffer changes --- addons/xterm-addon-search/src/SearchAddon.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 33e38e95e8..457e085305 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -53,6 +53,7 @@ export class SearchAddon implements ITerminalAddon { private _resultDecorations: Map = new Map(); private _searchResults: Map = new Map(); private _onDataDisposable: IDisposable | undefined; + private _lastSearchOptions: ISearchOptions | undefined; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -65,7 +66,13 @@ export class SearchAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - this._onDataDisposable = this._terminal.onData(() => this._dataChanged = true); + this._onDataDisposable = this._terminal.onData(() => { + //TODO: debounce + this._dataChanged = true; + if (this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) { + this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions,'previous'); + } + }); } public dispose(): void { @@ -94,10 +101,11 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } + this._lastSearchOptions = searchOptions; return searchOptions?.decorations ? this._highlightAllMatches(term, searchOptions, 'next') : this._findAndSelectNext(term, searchOptions); } - private _highlightAllMatches(term: string, searchOptions: ISearchOptions, type: 'next' | 'previous'): boolean { + private _highlightAllMatches(term: string, searchOptions: ISearchOptions, selectionType: 'next' | 'previous'): boolean { if (!this._terminal) { throw new Error('cannot find all matches with no terminal'); } @@ -107,7 +115,7 @@ export class SearchAddon implements ITerminalAddon { } searchOptions = searchOptions || {}; if (term === this._cachedSearchTerm && !this._dataChanged) { - return type === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); + return selectionType === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); } // new search, clear out the old decorations this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); @@ -118,12 +126,12 @@ export class SearchAddon implements ITerminalAddon { this._terminal.options.overviewRulerWidth = 10; } searchOptions.incremental = false; - let found = type === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); + let found = selectionType === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); while (found && (!this._result || !this._searchResults.get(`${this._result.row}-${this._result.col}`))) { if (this._result) { this._searchResults.set(`${this._result.row}-${this._result.col}`, this._result); } - found = type === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); + found = selectionType === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); } this._searchResults.forEach(result => { const resultDecoration = this._createResultDecoration(result, searchOptions.decorations); @@ -221,6 +229,7 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } + this._lastSearchOptions = searchOptions; return searchOptions?.decorations ? this._highlightAllMatches(term, searchOptions, 'previous') : this._findAndSelectPrevious(term, searchOptions); } From 1098173908af0894e2637467ea9b233debe3c6c5 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 11:45:45 -0400 Subject: [PATCH 35/55] use set timeout --- addons/xterm-addon-search/src/SearchAddon.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 457e085305..4bca88ccdf 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -67,11 +67,12 @@ export class SearchAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; this._onDataDisposable = this._terminal.onData(() => { - //TODO: debounce this._dataChanged = true; - if (this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) { - this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions,'previous'); - } + setTimeout(() => { + if (this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) { + this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions,'previous'); + } + }, 200); }); } From 97e4af3707e2343d0d8770d3dbed2a03929bbc02 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 13:07:25 -0400 Subject: [PATCH 36/55] only highlight all matches when decorations are requested --- addons/xterm-addon-search/src/SearchAddon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 4bca88ccdf..6e4b5f9bc7 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -69,7 +69,7 @@ export class SearchAddon implements ITerminalAddon { this._onDataDisposable = this._terminal.onData(() => { this._dataChanged = true; setTimeout(() => { - if (this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) { + if (this._lastSearchOptions?.decorations && this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) { this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions,'previous'); } }, 200); From 4cea6b3f4136f5626a00229b68ade83076776971 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 13:35:56 -0400 Subject: [PATCH 37/55] revert a breaking change --- addons/xterm-addon-search/src/SearchAddon.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 6e4b5f9bc7..6b84421d14 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -169,9 +169,6 @@ export class SearchAddon implements ITerminalAddon { currentSelection = this._terminal.getSelectionPosition()!; startRow = incremental ? currentSelection.startRow : currentSelection.endRow; startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn; - } else if (!startRow) { - startRow = this._terminal.buffer.active.cursorY; - startCol = this._terminal.buffer.active.cursorX; } this._initLinesCache(); From 64d5af38f5fbea10958c205ebb7556d19f107da0 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 21 Mar 2022 15:35:01 -0400 Subject: [PATCH 38/55] Update addons/xterm-addon-search/src/SearchAddon.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- addons/xterm-addon-search/src/SearchAddon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 6b84421d14..c2089f11cc 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -70,7 +70,7 @@ export class SearchAddon implements ITerminalAddon { this._dataChanged = true; setTimeout(() => { if (this._lastSearchOptions?.decorations && this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) { - this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions,'previous'); + this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions, 'previous'); } }, 200); }); From e8aa4eba75522877af05799020b91b7b2829ab76 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 15:36:39 -0400 Subject: [PATCH 39/55] IDecorationColor -> ISearchDecorationOptions --- addons/xterm-addon-search/src/SearchAddon.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 6b84421d14..bf4d633019 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -10,10 +10,10 @@ export interface ISearchOptions { wholeWord?: boolean; caseSensitive?: boolean; incremental?: boolean; - decorations?: IDecorationColor; + decorations?: ISearchDecorationOptions; } -interface IDecorationColor { +interface ISearchDecorationOptions { matchColor: string; selectedColor: string; } @@ -541,7 +541,7 @@ export class SearchAddon implements ITerminalAddon { * @param result The result to select. * @return Whether a result was selected. */ - private _selectResult(result: ISearchResult | undefined, decorations?: IDecorationColor): boolean { + private _selectResult(result: ISearchResult | undefined, decorations?: ISearchDecorationOptions): boolean { const terminal = this._terminal!; this._selectedDecoration?.dispose(); if (!result) { @@ -592,7 +592,7 @@ export class SearchAddon implements ITerminalAddon { * @param color the color to use for the decoration * @returns the {@link IDecoration} or undefined if the marker has already been disposed of */ - private _createResultDecoration(result: ISearchResult, decorations?: IDecorationColor): IDecoration | undefined { + private _createResultDecoration(result: ISearchResult, decorations?: ISearchDecorationOptions): IDecoration | undefined { const terminal = this._terminal!; const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); if (!marker || !decorations?.matchColor) { From 7a502c6b83ff1fdd32c9f8d76343b680cfa926e8 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 15:43:08 -0400 Subject: [PATCH 40/55] use const enum --- addons/xterm-addon-search/src/SearchAddon.ts | 19 ++++++++++++------- .../typings/xterm-addon-search.d.ts | 8 -------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 46c32c7f72..2f3fbb6e23 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -5,6 +5,11 @@ import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm'; +const enum SelectionType { + NEXT = 0, + PREVIOUS = 1 +} + export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; @@ -70,7 +75,7 @@ export class SearchAddon implements ITerminalAddon { this._dataChanged = true; setTimeout(() => { if (this._lastSearchOptions?.decorations && this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) { - this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions, 'previous'); + this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions, SelectionType.PREVIOUS); } }, 200); }); @@ -103,10 +108,10 @@ export class SearchAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } this._lastSearchOptions = searchOptions; - return searchOptions?.decorations ? this._highlightAllMatches(term, searchOptions, 'next') : this._findAndSelectNext(term, searchOptions); + return searchOptions?.decorations ? this._highlightAllMatches(term, searchOptions, SelectionType.NEXT) : this._findAndSelectNext(term, searchOptions); } - private _highlightAllMatches(term: string, searchOptions: ISearchOptions, selectionType: 'next' | 'previous'): boolean { + private _highlightAllMatches(term: string, searchOptions: ISearchOptions, selectionType: SelectionType): boolean { if (!this._terminal) { throw new Error('cannot find all matches with no terminal'); } @@ -116,7 +121,7 @@ export class SearchAddon implements ITerminalAddon { } searchOptions = searchOptions || {}; if (term === this._cachedSearchTerm && !this._dataChanged) { - return selectionType === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); + return selectionType === SelectionType.NEXT ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); } // new search, clear out the old decorations this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); @@ -127,12 +132,12 @@ export class SearchAddon implements ITerminalAddon { this._terminal.options.overviewRulerWidth = 10; } searchOptions.incremental = false; - let found = selectionType === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); + let found = selectionType === SelectionType.NEXT ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); while (found && (!this._result || !this._searchResults.get(`${this._result.row}-${this._result.col}`))) { if (this._result) { this._searchResults.set(`${this._result.row}-${this._result.col}`, this._result); } - found = selectionType === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); + found = selectionType === SelectionType.NEXT ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); } this._searchResults.forEach(result => { const resultDecoration = this._createResultDecoration(result, searchOptions.decorations); @@ -228,7 +233,7 @@ export class SearchAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } this._lastSearchOptions = searchOptions; - return searchOptions?.decorations ? this._highlightAllMatches(term, searchOptions, 'previous') : this._findAndSelectPrevious(term, searchOptions); + return searchOptions?.decorations ? this._highlightAllMatches(term, searchOptions, SelectionType.PREVIOUS) : this._findAndSelectPrevious(term, searchOptions); } private _findAndSelectPrevious(term: string, searchOptions?: ISearchOptions): boolean { 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 228a5ffeb2..ef2948a5ab 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -49,14 +49,6 @@ declare module 'xterm-addon-search' { */ public dispose(): void; - /** - * Find all instances of the term, selecting the next one with each - * enter. If it doesn't exist, do nothing. - * @param term The search term. - * @param searchOptions The options for the search. - */ - public find(term: string, searchOptions?: ISearchOptions): boolean; - /** * Search forwards for the next result that matches the search term and * options. From fefdd8a4e9cbb8f8f2216b32b0a5558aafa3f05b Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 15:44:17 -0400 Subject: [PATCH 41/55] clearDecorations --- addons/xterm-addon-search/src/SearchAddon.ts | 10 +++++----- .../xterm-addon-search/typings/xterm-addon-search.d.ts | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 2f3fbb6e23..bc27e4e486 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -82,11 +82,11 @@ export class SearchAddon implements ITerminalAddon { } public dispose(): void { - this.clear(); + this.clearDecorations(); this._onDataDisposable?.dispose(); } - public clear(): void { + public clearDecorations(): void { this._selectedDecoration?.dispose(); this._terminal?.clearSelection(); this._searchResults.clear(); @@ -116,7 +116,7 @@ export class SearchAddon implements ITerminalAddon { throw new Error('cannot find all matches with no terminal'); } if (!term || term.length === 0) { - this.clear(); + this.clearDecorations(); return false; } searchOptions = searchOptions || {}; @@ -160,7 +160,7 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal || !term || term.length === 0) { this._result = undefined; this._terminal?.clearSelection(); - this.clear(); + this.clearDecorations(); return false; } @@ -244,7 +244,7 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal || !term || term.length === 0) { this._result = undefined; this._terminal?.clearSelection(); - this.clear(); + this.clearDecorations(); return false; } 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 ef2948a5ab..e58afec002 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -64,5 +64,10 @@ declare module 'xterm-addon-search' { * @param searchOptions The options for the search. */ public findPrevious(term: string, searchOptions?: ISearchOptions): boolean; + + /** + * Clears the decorations and selection + */ + public clearDecorations(): void; } } From 866ee23df9ab7dd0bd3185255307e6cbba15eb14 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 15:45:45 -0400 Subject: [PATCH 42/55] add disposeDecorations helper --- addons/xterm-addon-search/src/SearchAddon.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index bc27e4e486..5451b832aa 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -90,12 +90,16 @@ export class SearchAddon implements ITerminalAddon { this._selectedDecoration?.dispose(); this._terminal?.clearSelection(); this._searchResults.clear(); - this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); - this._resultDecorations.clear(); + this._disposeDecorations(); this._cachedSearchTerm = undefined; this._dataChanged = true; } + private _disposeDecorations(): void { + this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); + this._resultDecorations.clear(); + } + /** * Find the next instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. @@ -124,8 +128,7 @@ export class SearchAddon implements ITerminalAddon { return selectionType === SelectionType.NEXT ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); } // new search, clear out the old decorations - this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); - this._resultDecorations.clear(); + this._disposeDecorations(); this._searchResults.clear(); if (!this._terminal.options.overviewRulerWidth) { From 4645ef706ee524cf96de0916c5842547cd4ff2c8 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 18:13:08 -0400 Subject: [PATCH 43/55] refactor --- addons/xterm-addon-search/src/SearchAddon.ts | 145 ++++++++++++------- 1 file changed, 92 insertions(+), 53 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 5451b832aa..7a20c5c99e 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -5,11 +5,6 @@ import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm'; -const enum SelectionType { - NEXT = 0, - PREVIOUS = 1 -} - export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; @@ -51,7 +46,6 @@ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs export class SearchAddon implements ITerminalAddon { private _terminal: Terminal | undefined; - private _result: ISearchResult | undefined; private _dataChanged: boolean = false; private _cachedSearchTerm: string | undefined; private _selectedDecoration: IDecoration | undefined; @@ -75,7 +69,7 @@ export class SearchAddon implements ITerminalAddon { this._dataChanged = true; setTimeout(() => { if (this._lastSearchOptions?.decorations && this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) { - this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions, SelectionType.PREVIOUS); + this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions); } }, 200); }); @@ -96,7 +90,11 @@ export class SearchAddon implements ITerminalAddon { } private _disposeDecorations(): void { - this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose())); + this._resultDecorations.forEach(decorations => { + for (const d of decorations) { + d.dispose(); + } + }); this._resultDecorations.clear(); } @@ -112,38 +110,38 @@ export class SearchAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } this._lastSearchOptions = searchOptions; - return searchOptions?.decorations ? this._highlightAllMatches(term, searchOptions, SelectionType.NEXT) : this._findAndSelectNext(term, searchOptions); + const findNextResult = this._findNextAndSelect(term, searchOptions); + if (searchOptions?.decorations) { + this._highlightAllMatches(term, searchOptions); + } + return findNextResult; } - private _highlightAllMatches(term: string, searchOptions: ISearchOptions, selectionType: SelectionType): boolean { + private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void { if (!this._terminal) { - throw new Error('cannot find all matches with no terminal'); + throw new Error('Cannot use addon until it has been loaded'); } if (!term || term.length === 0) { this.clearDecorations(); - return false; + return; } searchOptions = searchOptions || {}; if (term === this._cachedSearchTerm && !this._dataChanged) { - return selectionType === SelectionType.NEXT ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); + return; } // new search, clear out the old decorations this._disposeDecorations(); this._searchResults.clear(); - + 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); + result = this._find(term, result.row, result.col + 1, searchOptions); + } if (!this._terminal.options.overviewRulerWidth) { this._terminal.options.overviewRulerWidth = 10; } - searchOptions.incremental = false; - let found = selectionType === SelectionType.NEXT ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); - while (found && (!this._result || !this._searchResults.get(`${this._result.row}-${this._result.col}`))) { - if (this._result) { - this._searchResults.set(`${this._result.row}-${this._result.col}`, this._result); - } - found = selectionType === SelectionType.NEXT ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions); - } this._searchResults.forEach(result => { - const resultDecoration = this._createResultDecoration(result, searchOptions.decorations); + const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!); if (resultDecoration) { const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || []; decorationsForLine.push(resultDecoration); @@ -156,12 +154,49 @@ export class SearchAddon implements ITerminalAddon { if (this._searchResults.size > 0) { this._cachedSearchTerm = term; } - return this._searchResults.size > 0; } - private _findAndSelectNext(term: string, searchOptions?: ISearchOptions): boolean { + private _find(term: string, startRow?: number, startCol?: number, searchOptions?: ISearchOptions): ISearchResult | undefined { + if (!this._terminal || !term || term.length === 0) { + this._terminal?.clearSelection(); + this.clearDecorations(); + return undefined; + } + let result: ISearchResult | undefined = undefined; + startCol = startCol || 0; + startRow = startRow ?? 0; + + this._initLinesCache(); + + const searchPosition: ISearchPosition = { + startRow, + startCol + }; + + // Search startRow + result = this._findInLine(term, searchPosition, searchOptions); + // Search from startRow + 1 to end + if (!result) { + + for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) { + searchPosition.startRow = y; + searchPosition.startCol = 0; + // If the current line is wrapped line, increase index of column to ignore the previous scan + // Otherwise, reset beginning column index to zero with set new unwrapped line index + result = this._findInLine(term, searchPosition, searchOptions); + if (result) { + break; + } + } + } + if (result && searchOptions?.decorations) { + this._createResultDecoration(result, searchOptions?.decorations); + } + return result; + } + + private _findNextAndSelect(term: string, searchOptions?: ISearchOptions): boolean { if (!this._terminal || !term || term.length === 0) { - this._result = undefined; this._terminal?.clearSelection(); this.clearDecorations(); return false; @@ -187,42 +222,42 @@ export class SearchAddon implements ITerminalAddon { }; // Search startRow - this._result = this._findInLine(term, searchPosition, searchOptions); + let result = this._findInLine(term, searchPosition, searchOptions); // Search from startRow + 1 to end - if (!this._result) { + if (!result) { for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) { searchPosition.startRow = y; searchPosition.startCol = 0; // If the current line is wrapped line, increase index of column to ignore the previous scan // Otherwise, reset beginning column index to zero with set new unwrapped line index - this._result = this._findInLine(term, searchPosition, searchOptions); - if (this._result) { + result = this._findInLine(term, searchPosition, searchOptions); + if (result) { break; } } } // If we hit the bottom and didn't search from the very top wrap back up - if (!this._result && startRow !== 0) { + if (!result && startRow !== 0) { for (let y = 0; y < startRow; y++) { searchPosition.startRow = y; searchPosition.startCol = 0; - this._result = this._findInLine(term, searchPosition, searchOptions); - if (this._result) { + result = this._findInLine(term, searchPosition, searchOptions); + if (result) { break; } } } // If there is only one result, wrap back and return selection if it exists. - if (!this._result && currentSelection) { + if (!result && currentSelection) { searchPosition.startRow = currentSelection.startRow; searchPosition.startCol = 0; - this._result = this._findInLine(term, searchPosition, searchOptions); + result = this._findInLine(term, searchPosition, searchOptions); } // Set selection and scroll if a result was found - return this._selectResult(this._result, searchOptions?.decorations); + return this._selectResult(result, searchOptions?.decorations); } /** * Find the previous instance of the term, then scroll to and select it. If it @@ -236,16 +271,20 @@ export class SearchAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } this._lastSearchOptions = searchOptions; - return searchOptions?.decorations ? this._highlightAllMatches(term, searchOptions, SelectionType.PREVIOUS) : this._findAndSelectPrevious(term, searchOptions); + const findPreviousResult = this._findAndSelectPrevious(term, searchOptions); + if (searchOptions?.decorations) { + this._highlightAllMatches(term, searchOptions); + } + return findPreviousResult; } private _findAndSelectPrevious(term: string, searchOptions?: ISearchOptions): boolean { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - + let result: ISearchResult | undefined; if (!this._terminal || !term || term.length === 0) { - this._result = undefined; + result = undefined; this._terminal?.clearSelection(); this.clearDecorations(); return false; @@ -272,47 +311,47 @@ export class SearchAddon implements ITerminalAddon { if (incremental) { // Try to expand selection to right first. - this._result = this._findInLine(term, searchPosition, searchOptions, false); - const isOldResultHighlighted = this._result && this._result.row === startRow && this._result.col === startCol; + result = this._findInLine(term, searchPosition, searchOptions, false); + const isOldResultHighlighted = result && result.row === startRow && result.col === startCol; if (!isOldResultHighlighted) { // If selection was not able to be expanded to the right, then try reverse search if (currentSelection) { searchPosition.startRow = currentSelection.endRow; searchPosition.startCol = currentSelection.endColumn; } - this._result = this._findInLine(term, searchPosition, searchOptions, true); + result = this._findInLine(term, searchPosition, searchOptions, true); } } else { - this._result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); } // Search from startRow - 1 to top - if (!this._result) { + if (!result) { searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols); for (let y = startRow - 1; y >= 0; y--) { searchPosition.startRow = y; - this._result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); - if (this._result) { + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); + if (result) { break; } } } // If we hit the top and didn't search from the very bottom wrap back down - if (!this._result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) { + if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) { for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) { searchPosition.startRow = y; - this._result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); - if (this._result) { + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); + if (result) { break; } } } // If there is only one result, return true. - if (!this._result && currentSelection) return true; + if (!result && currentSelection) return true; - // Set selection and scroll if a this._result was found - return this._selectResult(this._result, searchOptions?.decorations); + // Set selection and scroll if a result was found + return this._selectResult(result, searchOptions?.decorations); } /** @@ -600,7 +639,7 @@ export class SearchAddon implements ITerminalAddon { * @param color the color to use for the decoration * @returns the {@link IDecoration} or undefined if the marker has already been disposed of */ - private _createResultDecoration(result: ISearchResult, decorations?: ISearchDecorationOptions): IDecoration | undefined { + private _createResultDecoration(result: ISearchResult, decorations: ISearchDecorationOptions): IDecoration | undefined { const terminal = this._terminal!; const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); if (!marker || !decorations?.matchColor) { From 7f131aa19a526164cf9d738c04efd33c0828b97c Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 21 Mar 2022 18:16:35 -0400 Subject: [PATCH 44/55] fix error --- src/browser/Decorations/OverviewRulerRenderer.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/browser/Decorations/OverviewRulerRenderer.ts b/src/browser/Decorations/OverviewRulerRenderer.ts index aa09db568e..b15d0d012d 100644 --- a/src/browser/Decorations/OverviewRulerRenderer.ts +++ b/src/browser/Decorations/OverviewRulerRenderer.ts @@ -143,12 +143,10 @@ export class OverviewRulerRenderer extends Disposable { } this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); for (const decoration of this._decorationService.decorations) { - if (decoration.options.overviewRulerOptions!.position !== 'full') { - this._renderDecoration(decoration, updateAnchor); + if (!decoration.options.overviewRulerOptions?.position) { + continue; } - } - for (const decoration of this._decorationService.decorations) { - if (decoration.options.overviewRulerOptions!.position === 'full') { + if (decoration.options.overviewRulerOptions.position !== 'full') { this._renderDecoration(decoration, updateAnchor); } } From 7a70db709c98db5da16de106da27e4cbb4ef840a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 15:51:51 -0700 Subject: [PATCH 45/55] Fix API indentation --- .../typings/xterm-addon-search.d.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 e58afec002..794d49c1e5 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -55,19 +55,19 @@ declare module 'xterm-addon-search' { * @param term The search term. * @param searchOptions The options for the search. */ - public findNext(term: string, searchOptions?: ISearchOptions): boolean; + public findNext(term: string, searchOptions?: ISearchOptions): boolean; - /** - * Search backwards for the previous result that matches the search term and - * options. - * @param term The search term. - * @param searchOptions The options for the search. - */ - public findPrevious(term: string, searchOptions?: ISearchOptions): boolean; + /** + * Search backwards for the previous result that matches the search term and + * options. + * @param term The search term. + * @param searchOptions The options for the search. + */ + public findPrevious(term: string, searchOptions?: ISearchOptions): boolean; - /** - * Clears the decorations and selection - */ - public clearDecorations(): void; + /** + * Clears the decorations and selection + */ + public clearDecorations(): void; } } From 09d1a01126d9f53a6c6411ab6f9737752339dd83 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:01:13 -0700 Subject: [PATCH 46/55] Add ISearchDecorationOptions to the d.ts --- .../typings/xterm-addon-search.d.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 794d49c1e5..e620a25406 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -32,6 +32,27 @@ declare module 'xterm-addon-search' { * `findNext`, not `findPrevious`. */ incremental?: boolean; + + /** + * When set, will highlight all instances of the word on search and show + * them in the overview ruler if it's enabled. + */ + decorations?: ISearchDecorationOptions; + } + + /** + * Options for showing decorations when searching. + */ + interface ISearchDecorationOptions { + /** + * The color of a match. + */ + matchColor: string; + + /** + * The color for the currently selected match. + */ + selectedColor: string; } /** From ef3695f727f6a0cc9a02ed0f386ffab2f63b4fb1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:03:10 -0700 Subject: [PATCH 47/55] Move setting opacity into js --- addons/xterm-addon-search/src/SearchAddon.ts | 2 +- css/xterm.css | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 7a20c5c99e..982be04856 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -626,10 +626,10 @@ export class SearchAddon implements ITerminalAddon { } if (!element.classList.contains('xterm-find-result-decoration')) { element.classList.add('xterm-find-result-decoration'); - // decoration's clientWidth = actualCellWidth element.style.left = `${element.clientWidth * result.col}px`; element.style.width = `${element.clientWidth * result.term.length}px`; element.style.backgroundColor = color; + element.style.opacity = '0.6'; } } diff --git a/css/xterm.css b/css/xterm.css index b9382ccc72..7432fbb1a2 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -179,10 +179,6 @@ position: absolute; } -.xterm-find-result-decoration { - opacity: 0.6; -} - .xterm-decoration-overview-ruler { z-index: 7; position: absolute; From 87dd275a51ff0b8cf52a28805c8df1d369b66187 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:03:59 -0700 Subject: [PATCH 48/55] Undo change to Terminal.ts --- src/browser/Terminal.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 5803e8c473..8cb116ae4c 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -1017,6 +1017,7 @@ export class Terminal extends CoreTerminal implements ITerminal { if (this.buffer !== this.buffers.normal) { return; } + return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset); } From efda0e77a9f68e59f3ed3b862a7255479df85afd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:04:23 -0700 Subject: [PATCH 49/55] Fix indentation properly this time --- addons/xterm-addon-search/typings/xterm-addon-search.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e620a25406..67ed2985da 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -76,7 +76,7 @@ declare module 'xterm-addon-search' { * @param term The search term. * @param searchOptions The options for the search. */ - public findNext(term: string, searchOptions?: ISearchOptions): boolean; + public findNext(term: string, searchOptions?: ISearchOptions): boolean; /** * Search backwards for the previous result that matches the search term and From fe530d92ecc467adcd5c43301971f65dd275a726 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:11:31 -0700 Subject: [PATCH 50/55] Ensure marker is disposed when decoration is --- addons/xterm-addon-search/src/SearchAddon.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 982be04856..d8266e02a0 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -601,6 +601,7 @@ export class SearchAddon implements ITerminalAddon { if (marker) { this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: decorations.selectedColor } }); this._selectedDecoration?.onRender((e) => this._applyStyles(e, decorations.selectedColor, result)); + this._selectedDecoration?.onDispose(() => marker.dispose()); } } @@ -650,6 +651,7 @@ export class SearchAddon implements ITerminalAddon { overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color: decorations.matchColor, position: 'center' } }); findResultDecoration?.onRender((e) => this._applyStyles(e, decorations.matchColor, result)); + findResultDecoration?.onDispose(() => marker.dispose()); return findResultDecoration; } } From 23076ba6ccc73c91872bb4bc4bfd29f64920ce3a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:20:51 -0700 Subject: [PATCH 51/55] Fix duplicate decorations getting created --- addons/xterm-addon-search/src/SearchAddon.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index d8266e02a0..c3fc3ecc33 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -92,6 +92,7 @@ export class SearchAddon implements ITerminalAddon { private _disposeDecorations(): void { this._resultDecorations.forEach(decorations => { for (const d of decorations) { + console.log('dispose', d); d.dispose(); } }); @@ -189,9 +190,6 @@ export class SearchAddon implements ITerminalAddon { } } } - if (result && searchOptions?.decorations) { - this._createResultDecoration(result, searchOptions?.decorations); - } return result; } @@ -646,6 +644,7 @@ export class SearchAddon implements ITerminalAddon { if (!marker || !decorations?.matchColor) { return undefined; } + const findResultDecoration = terminal.registerDecoration( { marker, overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color: decorations.matchColor, position: 'center' } From f9deb2f8409de532c9ed4e10004c1f0830be6c3d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:34:48 -0700 Subject: [PATCH 52/55] Don't set overview ruler width when using find An embedder may not want to enable it --- addons/xterm-addon-search/src/SearchAddon.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index c3fc3ecc33..12e82103fa 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -92,7 +92,6 @@ export class SearchAddon implements ITerminalAddon { private _disposeDecorations(): void { this._resultDecorations.forEach(decorations => { for (const d of decorations) { - console.log('dispose', d); d.dispose(); } }); @@ -138,9 +137,6 @@ export class SearchAddon implements ITerminalAddon { this._searchResults.set(`${result.row}-${result.col}`, result); result = this._find(term, result.row, result.col + 1, searchOptions); } - if (!this._terminal.options.overviewRulerWidth) { - this._terminal.options.overviewRulerWidth = 10; - } this._searchResults.forEach(result => { const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!); if (resultDecoration) { @@ -644,7 +640,6 @@ export class SearchAddon implements ITerminalAddon { if (!marker || !decorations?.matchColor) { return undefined; } - const findResultDecoration = terminal.registerDecoration( { marker, overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color: decorations.matchColor, position: 'center' } From 6ef1bdd7ce04d733100e57eca0d989447bdad1d9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:37:46 -0700 Subject: [PATCH 53/55] Debounce onData listener --- addons/xterm-addon-search/src/SearchAddon.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 12e82103fa..3dd2992dac 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -53,6 +53,7 @@ export class SearchAddon implements ITerminalAddon { private _searchResults: Map = new Map(); private _onDataDisposable: IDisposable | undefined; private _lastSearchOptions: ISearchOptions | undefined; + private _highlightTimeout: number | undefined; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -67,7 +68,10 @@ export class SearchAddon implements ITerminalAddon { this._terminal = terminal; this._onDataDisposable = this._terminal.onData(() => { this._dataChanged = true; - setTimeout(() => { + 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); } From b39477d81acb67a0a061a9586a2db59706afb732 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:45:48 -0700 Subject: [PATCH 54/55] Fix full decorations --- src/browser/Decorations/OverviewRulerRenderer.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/browser/Decorations/OverviewRulerRenderer.ts b/src/browser/Decorations/OverviewRulerRenderer.ts index b15d0d012d..9cdf99caf5 100644 --- a/src/browser/Decorations/OverviewRulerRenderer.ts +++ b/src/browser/Decorations/OverviewRulerRenderer.ts @@ -143,10 +143,12 @@ export class OverviewRulerRenderer extends Disposable { } this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); for (const decoration of this._decorationService.decorations) { - if (!decoration.options.overviewRulerOptions?.position) { - continue; + if (decoration.options.overviewRulerOptions && decoration.options.overviewRulerOptions.position !== 'full') { + this._renderDecoration(decoration, updateAnchor); } - if (decoration.options.overviewRulerOptions.position !== 'full') { + } + for (const decoration of this._decorationService.decorations) { + if (decoration.options.overviewRulerOptions && decoration.options.overviewRulerOptions.position === 'full') { this._renderDecoration(decoration, updateAnchor); } } From 43a014e8c1e178c5647fa467363e6ac64e6ad8cb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:57:49 -0700 Subject: [PATCH 55/55] Fix decoration lifecycle issues --- src/common/services/DecorationService.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index 6c7502361c..fba5fc356b 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -32,7 +32,10 @@ export class DecorationService extends Disposable implements IDecorationService if (decoration) { decoration.onDispose(() => { if (decoration) { - this._decorations.splice(this._decorations.indexOf(decoration), 1); + const index = this._decorations.indexOf(decoration); + if (index >= 0) { + this._decorations.splice(this._decorations.indexOf(decoration), 1); + } } }); this._decorations.push(decoration); @@ -70,6 +73,10 @@ class Decoration extends Disposable implements IInternalDecoration { } } public override dispose(): void { + if (this._isDisposed) { + return; + } + this._isDisposed = true; this._onDispose.fire(); super.dispose(); }