Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show all matches when searching #3698

Merged
merged 63 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
60c11e8
start work
meganrogge Mar 11, 2022
9657442
get it to sort of work
meganrogge Mar 11, 2022
fa4479c
use decoration width, find all and select next
meganrogge Mar 11, 2022
372b6b6
set incremental search to false
meganrogge Mar 11, 2022
da2c59d
remove unused variable
meganrogge Mar 11, 2022
453cb00
use a class
meganrogge Mar 11, 2022
e7bbc41
start at cursor position and mandate a width gt 0
meganrogge Mar 11, 2022
b4de278
hide decorations >= bufferService rows
meganrogge Mar 11, 2022
f678f08
merge
meganrogge Mar 16, 2022
1bab173
fix problems
meganrogge Mar 16, 2022
40b8854
rm bad decoration service
meganrogge Mar 16, 2022
05eb12e
start work on selection color
meganrogge Mar 16, 2022
1939906
Merge branch 'xtermjs:master' into findAll
meganrogge Mar 16, 2022
26f8fce
update color when select next happens
meganrogge Mar 16, 2022
90f64ba
get it to work in the demo
meganrogge Mar 17, 2022
c2145d5
on dispose, clear the canvas at that position
meganrogge Mar 17, 2022
de56d04
return decoration from show result
meganrogge Mar 17, 2022
9bc5090
use a map
meganrogge Mar 18, 2022
c8dc679
Merge branch 'master' into findAll
meganrogge Mar 18, 2022
a1468d0
use start row and col
meganrogge Mar 18, 2022
6c4f8b4
append to results
meganrogge Mar 18, 2022
47ec313
support colors
meganrogge Mar 18, 2022
01fd148
clean up
meganrogge Mar 18, 2022
d2913d6
Merge branch 'master' into findAll
meganrogge Mar 18, 2022
8e68c67
applyStyles
meganrogge Mar 18, 2022
66dcd16
clean up jsdoc
meganrogge Mar 18, 2022
15c2432
jsdoc
meganrogge Mar 18, 2022
85683a4
add to col, not row
meganrogge Mar 18, 2022
dd50378
Update css/xterm.css
meganrogge Mar 18, 2022
8328fca
track disposable
meganrogge Mar 18, 2022
61c2ca7
Metrge branch 'findAll' of https://github.com/meganrogge/xterm.js int…
meganrogge Mar 18, 2022
fef30bd
revert row register marker changes
meganrogge Mar 18, 2022
61e5c10
pull apart and refactor findNext
meganrogge Mar 18, 2022
e79c21d
get it to work for previous too
meganrogge Mar 18, 2022
a0aae62
get rid of start col end col
meganrogge Mar 18, 2022
8225ecd
refactor from highlightAllMatches -> decorations
meganrogge Mar 21, 2022
12a999d
get rid of start row/col code
meganrogge Mar 21, 2022
bec4cb3
set data changed to false if true
meganrogge Mar 21, 2022
8ff34a6
update decorations dynamically when the buffer changes
meganrogge Mar 21, 2022
1098173
use set timeout
meganrogge Mar 21, 2022
faa3769
Merge branch 'master' into findAll
meganrogge Mar 21, 2022
97e4af3
only highlight all matches when decorations are requested
meganrogge Mar 21, 2022
b9331ec
Merge branch 'findAll' of https://github.com/meganrogge/xterm.js into…
meganrogge Mar 21, 2022
4cea6b3
revert a breaking change
meganrogge Mar 21, 2022
64d5af3
Update addons/xterm-addon-search/src/SearchAddon.ts
meganrogge Mar 21, 2022
e8aa4eb
IDecorationColor -> ISearchDecorationOptions
meganrogge Mar 21, 2022
90dca7d
Merge branch 'findAll' of https://github.com/meganrogge/xterm.js into…
meganrogge Mar 21, 2022
7a502c6
use const enum
meganrogge Mar 21, 2022
fefdd8a
clearDecorations
meganrogge Mar 21, 2022
866ee23
add disposeDecorations helper
meganrogge Mar 21, 2022
4645ef7
refactor
meganrogge Mar 21, 2022
7f131aa
fix error
meganrogge Mar 21, 2022
7a70db7
Fix API indentation
Tyriar Mar 21, 2022
09d1a01
Add ISearchDecorationOptions to the d.ts
Tyriar Mar 21, 2022
ef3695f
Move setting opacity into js
Tyriar Mar 21, 2022
87dd275
Undo change to Terminal.ts
Tyriar Mar 21, 2022
efda0e7
Fix indentation properly this time
Tyriar Mar 21, 2022
fe530d9
Ensure marker is disposed when decoration is
Tyriar Mar 21, 2022
23076ba
Fix duplicate decorations getting created
Tyriar Mar 21, 2022
f9deb2f
Don't set overview ruler width when using find
Tyriar Mar 21, 2022
6ef1bdd
Debounce onData listener
Tyriar Mar 21, 2022
b39477d
Fix full decorations
Tyriar Mar 21, 2022
43a014e
Fix decoration lifecycle issues
Tyriar Mar 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 177 additions & 35 deletions addons/xterm-addon-search/src/SearchAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
* @license MIT
*/

import { Terminal, IBufferLine, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm';

export interface ISearchOptions {
regex?: boolean;
wholeWord?: boolean;
caseSensitive?: boolean;
incremental?: boolean;
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
decorations?: IDecorationColor;
}

interface IDecorationColor {
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
matchColor: string;
selectedColor: string;
}

export interface ISearchPosition {
Expand Down Expand Up @@ -40,7 +46,14 @@ 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;
private _resultDecorations: Map<number, IDecoration[]> = new Map<number, IDecoration[]>();
private _searchResults: Map<string, ISearchResult> = 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.
Expand All @@ -53,9 +66,30 @@ export class SearchAddon implements ITerminalAddon {

public activate(terminal: Terminal): void {
this._terminal = terminal;
this._onDataDisposable = this._terminal.onData(() => {
this._dataChanged = true;
setTimeout(() => {
if (this._lastSearchOptions?.decorations && this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) {
this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions,'previous');
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
}
}, 200);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This create a new timeout on every onData event, there should only be a single timeout active at any time.

});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should get debounced to ensure we're not doing this every 200 ms, but only 200ms after no event

}

public dispose(): void { }
public dispose(): void {
this.clear();
this._onDataDisposable?.dispose();
}

public clear(): void {
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
this._selectedDecoration?.dispose();
this._terminal?.clearSelection();
this._searchResults.clear();
this._resultDecorations.forEach(decorations => decorations.forEach(d=> d.dispose()));
this._resultDecorations.clear();
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
this._cachedSearchTerm = undefined;
this._dataChanged = true;
}

/**
* Find the next instance of the term, then scroll to and select it. If it
Expand All @@ -68,9 +102,60 @@ 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, selectionType: 'next' | 'previous'): boolean {
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
if (!this._terminal) {
throw new Error('cannot find all matches with no terminal');
}
if (!term || term.length === 0) {
this._terminal.clearSelection();
this.clear();
return false;
}
searchOptions = searchOptions || {};
if (term === this._cachedSearchTerm && !this._dataChanged) {
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()));
this._resultDecorations.clear();
this._searchResults.clear();

if (!this._terminal.options.overviewRulerWidth) {
this._terminal.options.overviewRulerWidth = 10;
}
searchOptions.incremental = false;
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 = selectionType === 'next' ? this._findAndSelectNext(term, searchOptions) : this._findAndSelectPrevious(term, searchOptions);
}
this._searchResults.forEach(result => {
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations);
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 this._searchResults.size > 0;
}

private _findAndSelectNext(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal || !term || term.length === 0) {
this._result = undefined;
this._terminal?.clearSelection();
this.clear();
return false;
}

Expand All @@ -94,45 +179,43 @@ 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 this._selectResult(result);
return this._selectResult(this._result, searchOptions?.decorations);
}

/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
Expand All @@ -144,16 +227,26 @@ 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);
}

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;
const isReverseSearch = true;

const incremental = searchOptions ? searchOptions.incremental : false;
let currentSelection: ISelectionPosition | undefined;
if (this._terminal.hasSelection()) {
Expand All @@ -171,47 +264,47 @@ 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);
// Set selection and scroll if a this._result was found
return this._selectResult(this._result, searchOptions?.decorations);
}

/**
Expand Down Expand Up @@ -446,15 +539,24 @@ 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): boolean {
private _selectResult(result: ISearchResult | undefined, decorations?: IDecorationColor): boolean {
const terminal = this._terminal!;
this._selectedDecoration?.dispose();
if (!result) {
terminal.clearSelection();
return false;
}
terminal.select(result.col, result.row, result.size);
if (decorations?.selectedColor) {
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
if (marker) {
this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: decorations.selectedColor } });
this._selectedDecoration?.onRender((e) => this._applyStyles(e, decorations.selectedColor, result));
}
}

// 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;
Expand All @@ -463,4 +565,44 @@ 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;
}
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;
}
}

/**
* 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 _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);
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
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' }
});
findResultDecoration?.onRender((e) => this._applyStyles(e, decorations.matchColor, result));
return findResultDecoration;
}
}
18 changes: 13 additions & 5 deletions addons/xterm-addon-search/typings/xterm-addon-search.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
meganrogge marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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;
}
}
4 changes: 4 additions & 0 deletions css/xterm.css
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@
position: absolute;
}

.xterm-find-result-decoration {
opacity: 0.6;
}

Tyriar marked this conversation as resolved.
Show resolved Hide resolved
.xterm-decoration-overview-ruler {
z-index: 7;
position: absolute;
Expand Down
Loading