@xterm/addon-search 0.16.0-beta.12 → 0.16.0-beta.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,8 +5,8 @@
5
5
 
6
6
  import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm';
7
7
  import type { SearchAddon as ISearchApi } from '@xterm/addon-search';
8
- import { EventEmitter } from 'common/EventEmitter';
9
- import { Disposable, toDisposable, disposeArray, MutableDisposable, getDisposeArrayDisposable } from 'common/Lifecycle';
8
+ import { Emitter, Event } from 'vs/base/common/event';
9
+ import { combinedDisposable, Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
10
10
 
11
11
  export interface ISearchOptions {
12
12
  regex?: boolean;
@@ -31,6 +31,11 @@ export interface ISearchPosition {
31
31
  startRow: number;
32
32
  }
33
33
 
34
+ export interface ISearchResultChangeEvent {
35
+ resultIndex: number;
36
+ resultCount: number;
37
+ }
38
+
34
39
  export interface ISearchAddonOptions {
35
40
  highlightLimit: number;
36
41
  }
@@ -58,6 +63,11 @@ interface IHighlight extends IDisposable {
58
63
  match: ISearchResult;
59
64
  }
60
65
 
66
+ interface IMultiHighlight extends IDisposable {
67
+ decorations: IDecoration[];
68
+ match: ISearchResult;
69
+ }
70
+
61
71
  const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
62
72
  const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
63
73
  const DEFAULT_HIGHLIGHT_LIMIT = 1000;
@@ -67,7 +77,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
67
77
  private _cachedSearchTerm: string | undefined;
68
78
  private _highlightedLines: Set<number> = new Set();
69
79
  private _highlightDecorations: IHighlight[] = [];
70
- private _selectedDecoration: MutableDisposable<IHighlight> = this.register(new MutableDisposable());
80
+ private _searchResultsWithHighlight: ISearchResult[] = [];
81
+ private _selectedDecoration: MutableDisposable<IMultiHighlight> = this._register(new MutableDisposable());
71
82
  private _highlightLimit: number;
72
83
  private _lastSearchOptions: ISearchOptions | undefined;
73
84
  private _highlightTimeout: number | undefined;
@@ -80,8 +91,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
80
91
  private _linesCacheTimeoutId = 0;
81
92
  private _linesCacheDisposables = new MutableDisposable();
82
93
 
83
- private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>());
84
- public readonly onDidChangeResults = this._onDidChangeResults.event;
94
+ private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
95
+ public get onDidChangeResults(): Event<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
85
96
 
86
97
  constructor(options?: Partial<ISearchAddonOptions>) {
87
98
  super();
@@ -91,9 +102,9 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
91
102
 
92
103
  public activate(terminal: Terminal): void {
93
104
  this._terminal = terminal;
94
- this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
95
- this.register(this._terminal.onResize(() => this._updateMatches()));
96
- this.register(toDisposable(() => this.clearDecorations()));
105
+ this._register(this._terminal.onWriteParsed(() => this._updateMatches()));
106
+ this._register(this._terminal.onResize(() => this._updateMatches()));
107
+ this._register(toDisposable(() => this.clearDecorations()));
97
108
  }
98
109
 
99
110
  private _updateMatches(): void {
@@ -111,8 +122,9 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
111
122
 
112
123
  public clearDecorations(retainCachedSearchTerm?: boolean): void {
113
124
  this._selectedDecoration.clear();
114
- disposeArray(this._highlightDecorations);
125
+ dispose(this._highlightDecorations);
115
126
  this._highlightDecorations = [];
127
+ this._searchResultsWithHighlight = [];
116
128
  this._highlightedLines.clear();
117
129
  if (!retainCachedSearchTerm) {
118
130
  this._cachedSearchTerm = undefined;
@@ -162,15 +174,14 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
162
174
  // new search, clear out the old decorations
163
175
  this.clearDecorations(true);
164
176
 
165
- const searchResultsWithHighlight: ISearchResult[] = [];
166
177
  let prevResult: ISearchResult | undefined = undefined;
167
178
  let result = this._find(term, 0, 0, searchOptions);
168
179
  while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
169
- if (searchResultsWithHighlight.length >= this._highlightLimit) {
180
+ if (this._searchResultsWithHighlight.length >= this._highlightLimit) {
170
181
  break;
171
182
  }
172
183
  prevResult = result;
173
- searchResultsWithHighlight.push(prevResult);
184
+ this._searchResultsWithHighlight.push(prevResult);
174
185
  result = this._find(
175
186
  term,
176
187
  prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
@@ -178,15 +189,21 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
178
189
  searchOptions
179
190
  );
180
191
  }
181
- for (const match of searchResultsWithHighlight) {
182
- const decoration = this._createResultDecoration(match, searchOptions.decorations!);
183
- if (decoration) {
184
- this._highlightedLines.add(decoration.marker.line);
185
- this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
192
+ for (const match of this._searchResultsWithHighlight) {
193
+ const decorations = this._createResultDecorations(match, searchOptions.decorations!, false);
194
+ if (decorations) {
195
+ for (const decoration of decorations) {
196
+ this._storeDecoration(decoration, match);
197
+ }
186
198
  }
187
199
  }
188
200
  }
189
201
 
202
+ private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {
203
+ this._highlightedLines.add(decoration.marker.line);
204
+ this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
205
+ }
206
+
190
207
  private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
191
208
  if (!this._terminal || !term || term.length === 0) {
192
209
  this._terminal?.clearSelection();
@@ -339,15 +356,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
339
356
  let resultIndex = -1;
340
357
  if (this._selectedDecoration.value) {
341
358
  const selectedMatch = this._selectedDecoration.value.match;
342
- for (let i = 0; i < this._highlightDecorations.length; i++) {
343
- const match = this._highlightDecorations[i].match;
359
+ for (let i = 0; i < this._searchResultsWithHighlight.length; i++) {
360
+ const match = this._searchResultsWithHighlight[i];
344
361
  if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
345
362
  resultIndex = i;
346
363
  break;
347
364
  }
348
365
  }
349
366
  }
350
- this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length });
367
+ this._onDidChangeResults.fire({ resultIndex, resultCount: this._searchResultsWithHighlight.length });
351
368
  }
352
369
  }
353
370
 
@@ -426,11 +443,11 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
426
443
  const terminal = this._terminal!;
427
444
  if (!this._linesCache) {
428
445
  this._linesCache = new Array(terminal.buffer.active.length);
429
- this._linesCacheDisposables.value = getDisposeArrayDisposable([
446
+ this._linesCacheDisposables.value = combinedDisposable(
430
447
  terminal.onLineFeed(() => this._destroyLinesCache()),
431
448
  terminal.onCursorMove(() => this._destroyLinesCache()),
432
449
  terminal.onResize(() => this._destroyLinesCache())
433
- ]);
450
+ );
434
451
  }
435
452
 
436
453
  window.clearTimeout(this._linesCacheTimeoutId);
@@ -499,12 +516,16 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
499
516
  const [stringLine, offsets] = cache;
500
517
 
501
518
  const offset = this._bufferColsToStringOffset(row, col);
502
- const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
503
- const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
519
+ let searchTerm = term;
520
+ let searchStringLine = stringLine;
521
+ if (!searchOptions.regex) {
522
+ searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
523
+ searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
524
+ }
504
525
 
505
526
  let resultIndex = -1;
506
527
  if (searchOptions.regex) {
507
- const searchRegex = RegExp(searchTerm, 'g');
528
+ const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');
508
529
  let foundTerm: RegExpExecArray | null;
509
530
  if (isReverseSearch) {
510
531
  // This loop will get the resultIndex of the _last_ regex match in the range 0..offset
@@ -662,25 +683,9 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
662
683
  }
663
684
  terminal.select(result.col, result.row, result.size);
664
685
  if (options) {
665
- const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
666
- if (marker) {
667
- const decoration = terminal.registerDecoration({
668
- marker,
669
- x: result.col,
670
- width: result.size,
671
- backgroundColor: options.activeMatchBackground,
672
- layer: 'top',
673
- overviewRulerOptions: {
674
- color: options.activeMatchColorOverviewRuler
675
- }
676
- });
677
- if (decoration) {
678
- const disposables: IDisposable[] = [];
679
- disposables.push(marker);
680
- disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true)));
681
- disposables.push(decoration.onDispose(() => disposeArray(disposables)));
682
- this._selectedDecoration.value = { decoration, match: result, dispose() { decoration.dispose(); } };
683
- }
686
+ const decorations = this._createResultDecorations(result, options, true);
687
+ if (decorations) {
688
+ this._selectedDecoration.value = { decorations, match: result, dispose() { dispose(decorations); } };
684
689
  }
685
690
  }
686
691
 
@@ -720,28 +725,45 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
720
725
  * @param options the options for the decoration
721
726
  * @returns the {@link IDecoration} or undefined if the marker has already been disposed of
722
727
  */
723
- private _createResultDecoration(result: ISearchResult, options: ISearchDecorationOptions): IDecoration | undefined {
728
+ private _createResultDecorations(result: ISearchResult, options: ISearchDecorationOptions, isActiveResult: boolean): IDecoration[] | undefined {
724
729
  const terminal = this._terminal!;
725
- const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
726
- if (!marker) {
727
- return undefined;
728
- }
729
- const findResultDecoration = terminal.registerDecoration({
730
- marker,
731
- x: result.col,
732
- width: result.size,
733
- backgroundColor: options.matchBackground,
734
- overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
735
- color: options.matchOverviewRuler,
736
- position: 'center'
730
+
731
+ // Gather decoration ranges for this match as it could wrap
732
+ const decorationRanges: [number, number, number][] = [];
733
+ let currentCol = result.col;
734
+ let remainingSize = result.size;
735
+ let markerOffset = -terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row;
736
+ while (remainingSize > 0) {
737
+ const amountThisRow = Math.min(terminal.cols - currentCol, remainingSize);
738
+ decorationRanges.push([markerOffset, currentCol, amountThisRow]);
739
+ currentCol = 0;
740
+ remainingSize -= amountThisRow;
741
+ markerOffset++;
742
+ }
743
+
744
+ // Create the decorations
745
+ const decorations: IDecoration[] = [];
746
+ for (const range of decorationRanges) {
747
+ const marker = terminal.registerMarker(range[0]);
748
+ const decoration = terminal.registerDecoration({
749
+ marker,
750
+ x: range[1],
751
+ width: range[2],
752
+ backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,
753
+ overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
754
+ color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,
755
+ position: 'center'
756
+ }
757
+ });
758
+ if (decoration) {
759
+ const disposables: IDisposable[] = [];
760
+ disposables.push(marker);
761
+ disposables.push(decoration.onRender((e) => this._applyStyles(e, isActiveResult ? options.activeMatchBorder : options.matchBorder, false)));
762
+ disposables.push(decoration.onDispose(() => dispose(disposables)));
763
+ decorations.push(decoration);
737
764
  }
738
- });
739
- if (findResultDecoration) {
740
- const disposables: IDisposable[] = [];
741
- disposables.push(marker);
742
- disposables.push(findResultDecoration.onRender((e) => this._applyStyles(e, options.matchBorder, false)));
743
- disposables.push(findResultDecoration.onDispose(() => disposeArray(disposables)));
744
- }
745
- return findResultDecoration;
765
+ }
766
+
767
+ return decorations.length === 0 ? undefined : decorations;
746
768
  }
747
769
  }
@@ -75,6 +75,21 @@ declare module '@xterm/addon-search' {
75
75
  activeMatchColorOverviewRuler: string;
76
76
  }
77
77
 
78
+ /**
79
+ * Event data fired when search results change.
80
+ */
81
+ export interface ISearchResultChangeEvent {
82
+ /**
83
+ * The index of the currently active result, -1 when the threshold of matches is exceeded.
84
+ */
85
+ resultIndex: number;
86
+
87
+ /**
88
+ * The total number of search results found.
89
+ */
90
+ resultCount: number;
91
+ }
92
+
78
93
  /**
79
94
  * Options for the search addon.
80
95
  */
@@ -139,8 +154,7 @@ declare module '@xterm/addon-search' {
139
154
  /**
140
155
  * When decorations are enabled, fires when
141
156
  * the search results change.
142
- * @returns -1 for resultIndex when the threshold of matches is exceeded.
143
157
  */
144
- readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>;
158
+ readonly onDidChangeResults: IEvent<ISearchResultChangeEvent>;
145
159
  }
146
160
  }