@xterm/addon-search 0.16.0-beta.127 → 0.16.0-beta.129

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.
@@ -3,50 +3,25 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
- import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm';
7
- import type { SearchAddon as ISearchApi, ISearchOptions, ISearchDecorationOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search';
8
- import { Emitter, Event } from 'vs/base/common/event';
9
- import { Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
6
+ import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
7
+ import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search';
8
+ import { Event } from 'vs/base/common/event';
9
+ import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
10
10
  import { disposableTimeout } from 'vs/base/common/async';
11
11
  import { SearchLineCache } from './SearchLineCache';
12
+ import { SearchState } from './SearchState';
13
+ import { SearchEngine, type ISearchResult } from './SearchEngine';
14
+ import { DecorationManager } from './DecorationManager';
15
+ import { SearchResultTracker } from './SearchResultTracker';
12
16
 
13
17
  interface IInternalSearchOptions {
14
18
  noScroll: boolean;
15
19
  }
16
20
 
17
- interface ISearchPosition {
18
- startCol: number;
19
- startRow: number;
20
- }
21
-
22
- interface ISearchResult {
23
- term: string;
24
- col: number;
25
- row: number;
26
- size: number;
27
- }
28
-
29
- interface IHighlight extends IDisposable {
30
- decoration: IDecoration;
31
- match: ISearchResult;
32
- }
33
-
34
- interface IMultiHighlight extends IDisposable {
35
- decorations: IDecoration[];
36
- match: ISearchResult;
37
- }
38
-
39
21
  /**
40
22
  * Configuration constants for the search addon functionality.
41
23
  */
42
24
  const enum Constants {
43
- /**
44
- * Characters that are considered non-word characters for search boundary detection. These
45
- * characters are used to determine word boundaries when performing whole-word searches. Includes
46
- * common punctuation, symbols, and whitespace characters.
47
- */
48
- NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?',
49
-
50
25
  /**
51
26
  * Default maximum number of search results to highlight simultaneously. This limit prevents
52
27
  * performance degradation when searching for very common terms that would result in excessive
@@ -57,18 +32,19 @@ const enum Constants {
57
32
 
58
33
  export class SearchAddon extends Disposable implements ITerminalAddon, ISearchApi {
59
34
  private _terminal: Terminal | undefined;
60
- private _cachedSearchTerm: string | undefined;
61
- private _highlightedLines: Set<number> = new Set();
62
- private _highlightDecorations: IHighlight[] = [];
63
- private _searchResultsWithHighlight: ISearchResult[] = [];
64
- private _selectedDecoration = this._register(new MutableDisposable<IMultiHighlight>());
65
35
  private _highlightLimit: number;
66
- private _lastSearchOptions: ISearchOptions | undefined;
67
36
  private _highlightTimeout = this._register(new MutableDisposable<IDisposable>());
68
37
  private _lineCache = this._register(new MutableDisposable<SearchLineCache>());
69
38
 
70
- private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
71
- public get onDidChangeResults(): Event<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
39
+ // Component instances
40
+ private _state = new SearchState();
41
+ private _engine: SearchEngine | undefined;
42
+ private _decorationManager: DecorationManager | undefined;
43
+ private _resultTracker = this._register(new SearchResultTracker());
44
+
45
+ public get onDidChangeResults(): Event<ISearchResultChangeEvent> {
46
+ return this._resultTracker.onDidChangeResults;
47
+ }
72
48
 
73
49
  constructor(options?: Partial<ISearchAddonOptions>) {
74
50
  super();
@@ -79,6 +55,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
79
55
  public activate(terminal: Terminal): void {
80
56
  this._terminal = terminal;
81
57
  this._lineCache.value = new SearchLineCache(terminal);
58
+ this._engine = new SearchEngine(terminal, this._lineCache.value);
59
+ this._decorationManager = new DecorationManager(terminal);
82
60
  this._register(this._terminal.onWriteParsed(() => this._updateMatches()));
83
61
  this._register(this._terminal.onResize(() => this._updateMatches()));
84
62
  this._register(toDisposable(() => this.clearDecorations()));
@@ -86,28 +64,26 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
86
64
 
87
65
  private _updateMatches(): void {
88
66
  this._highlightTimeout.clear();
89
- if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
67
+ if (this._state.cachedSearchTerm && this._state.lastSearchOptions?.decorations) {
90
68
  this._highlightTimeout.value = disposableTimeout(() => {
91
- const term = this._cachedSearchTerm;
92
- this._cachedSearchTerm = undefined;
93
- this.findPrevious(term!, { ...this._lastSearchOptions, incremental: true }, { noScroll: true });
69
+ const term = this._state.cachedSearchTerm;
70
+ this._state.clearCachedTerm();
71
+ this.findPrevious(term!, { ...this._state.lastSearchOptions, incremental: true }, { noScroll: true });
94
72
  }, 200);
95
73
  }
96
74
  }
97
75
 
98
76
  public clearDecorations(retainCachedSearchTerm?: boolean): void {
99
- this._selectedDecoration.clear();
100
- dispose(this._highlightDecorations);
101
- this._highlightDecorations = [];
102
- this._searchResultsWithHighlight = [];
103
- this._highlightedLines.clear();
77
+ this._resultTracker.clearSelectedDecoration();
78
+ this._decorationManager?.clearHighlightDecorations();
79
+ this._resultTracker.clearResults();
104
80
  if (!retainCachedSearchTerm) {
105
- this._cachedSearchTerm = undefined;
81
+ this._state.clearCachedTerm();
106
82
  }
107
83
  }
108
84
 
109
85
  public clearActiveDecoration(): void {
110
- this._selectedDecoration.clear();
86
+ this._resultTracker.clearSelectedDecoration();
111
87
  }
112
88
 
113
89
  /**
@@ -118,172 +94,73 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
118
94
  * @returns Whether a result was found.
119
95
  */
120
96
  public findNext(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {
121
- if (!this._terminal) {
97
+ if (!this._terminal || !this._engine) {
122
98
  throw new Error('Cannot use addon until it has been loaded');
123
99
  }
124
- const didOptionsChanged = this._lastSearchOptions ? this._didOptionsChange(this._lastSearchOptions, searchOptions) : true;
125
- this._lastSearchOptions = searchOptions;
126
- if (searchOptions?.decorations) {
127
- if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm || didOptionsChanged) {
128
- this._highlightAllMatches(term, searchOptions);
129
- }
100
+
101
+ this._state.lastSearchOptions = searchOptions;
102
+
103
+ if (this._state.shouldUpdateHighlighting(term, searchOptions)) {
104
+ this._highlightAllMatches(term, searchOptions!);
130
105
  }
131
106
 
132
107
  const found = this._findNextAndSelect(term, searchOptions, internalSearchOptions);
133
108
  this._fireResults(searchOptions);
134
- this._cachedSearchTerm = term;
109
+ this._state.cachedSearchTerm = term;
135
110
 
136
111
  return found;
137
112
  }
138
113
 
139
114
  private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
140
- if (!this._terminal) {
115
+ if (!this._terminal || !this._engine || !this._decorationManager) {
141
116
  throw new Error('Cannot use addon until it has been loaded');
142
117
  }
143
- if (!term || term.length === 0) {
118
+ if (!this._state.isValidSearchTerm(term)) {
144
119
  this.clearDecorations();
145
120
  return;
146
121
  }
147
- searchOptions = searchOptions || {};
148
122
 
149
123
  // new search, clear out the old decorations
150
124
  this.clearDecorations(true);
151
125
 
126
+ const results: ISearchResult[] = [];
152
127
  let prevResult: ISearchResult | undefined = undefined;
153
- let result = this._find(term, 0, 0, searchOptions);
128
+ let result = this._engine.find(term, 0, 0, searchOptions);
129
+
154
130
  while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
155
- if (this._searchResultsWithHighlight.length >= this._highlightLimit) {
131
+ if (results.length >= this._highlightLimit) {
156
132
  break;
157
133
  }
158
134
  prevResult = result;
159
- this._searchResultsWithHighlight.push(prevResult);
160
- result = this._find(
135
+ results.push(prevResult);
136
+ result = this._engine.find(
161
137
  term,
162
138
  prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
163
139
  prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
164
140
  searchOptions
165
141
  );
166
142
  }
167
- for (const match of this._searchResultsWithHighlight) {
168
- const decorations = this._createResultDecorations(match, searchOptions.decorations!, false);
169
- if (decorations) {
170
- for (const decoration of decorations) {
171
- this._storeDecoration(decoration, match);
172
- }
173
- }
174
- }
175
- }
176
143
 
177
- private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {
178
- this._highlightedLines.add(decoration.marker.line);
179
- this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
180
- }
181
-
182
- private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
183
- if (!this._terminal || !term || term.length === 0) {
184
- this._terminal?.clearSelection();
185
- this.clearDecorations();
186
- return undefined;
144
+ this._resultTracker.updateResults(results, this._highlightLimit);
145
+ if (searchOptions.decorations) {
146
+ this._decorationManager.createHighlightDecorations(results, searchOptions.decorations);
187
147
  }
188
- if (startCol > this._terminal.cols) {
189
- throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
190
- }
191
-
192
- let result: ISearchResult | undefined = undefined;
193
-
194
- this._lineCache.value!.initLinesCache();
195
-
196
- const searchPosition: ISearchPosition = {
197
- startRow,
198
- startCol
199
- };
200
-
201
- // Search startRow
202
- result = this._findInLine(term, searchPosition, searchOptions);
203
- // Search from startRow + 1 to end
204
- if (!result) {
205
-
206
- for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
207
- searchPosition.startRow = y;
208
- searchPosition.startCol = 0;
209
- // If the current line is wrapped line, increase index of column to ignore the previous scan
210
- // Otherwise, reset beginning column index to zero with set new unwrapped line index
211
- result = this._findInLine(term, searchPosition, searchOptions);
212
- if (result) {
213
- break;
214
- }
215
- }
216
- }
217
- return result;
218
148
  }
219
149
 
220
150
  private _findNextAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {
221
- if (!this._terminal || !term || term.length === 0) {
222
- this._terminal?.clearSelection();
223
- this.clearDecorations();
151
+ if (!this._terminal || !this._engine) {
224
152
  return false;
225
153
  }
226
-
227
- const prevSelectedPos = this._terminal.getSelectionPosition();
228
- this._terminal.clearSelection();
229
-
230
- let startCol = 0;
231
- let startRow = 0;
232
- if (prevSelectedPos) {
233
- if (this._cachedSearchTerm === term) {
234
- startCol = prevSelectedPos.end.x;
235
- startRow = prevSelectedPos.end.y;
236
- } else {
237
- startCol = prevSelectedPos.start.x;
238
- startRow = prevSelectedPos.start.y;
239
- }
240
- }
241
-
242
- this._lineCache.value!.initLinesCache();
243
-
244
- const searchPosition: ISearchPosition = {
245
- startRow,
246
- startCol
247
- };
248
-
249
- // Search startRow
250
- let result = this._findInLine(term, searchPosition, searchOptions);
251
- // Search from startRow + 1 to end
252
- if (!result) {
253
-
254
- for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
255
- searchPosition.startRow = y;
256
- searchPosition.startCol = 0;
257
- // If the current line is wrapped line, increase index of column to ignore the previous scan
258
- // Otherwise, reset beginning column index to zero with set new unwrapped line index
259
- result = this._findInLine(term, searchPosition, searchOptions);
260
- if (result) {
261
- break;
262
- }
263
- }
264
- }
265
- // If we hit the bottom and didn't search from the very top wrap back up
266
- if (!result && startRow !== 0) {
267
- for (let y = 0; y < startRow; y++) {
268
- searchPosition.startRow = y;
269
- searchPosition.startCol = 0;
270
- result = this._findInLine(term, searchPosition, searchOptions);
271
- if (result) {
272
- break;
273
- }
274
- }
275
- }
276
-
277
- // If there is only one result, wrap back and return selection if it exists.
278
- if (!result && prevSelectedPos) {
279
- searchPosition.startRow = prevSelectedPos.start.y;
280
- searchPosition.startCol = 0;
281
- result = this._findInLine(term, searchPosition, searchOptions);
154
+ if (!this._state.isValidSearchTerm(term)) {
155
+ this._terminal.clearSelection();
156
+ this.clearDecorations();
157
+ return false;
282
158
  }
283
159
 
284
- // Set selection and scroll if a result was found
160
+ const result = this._engine.findNextWithSelection(term, searchOptions, this._state.cachedSearchTerm);
285
161
  return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);
286
162
  }
163
+
287
164
  /**
288
165
  * Find the previous instance of the term, then scroll to and select it. If it
289
166
  * doesn't exist, do nothing.
@@ -292,391 +169,73 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
292
169
  * @returns Whether a result was found.
293
170
  */
294
171
  public findPrevious(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {
295
- if (!this._terminal) {
172
+ if (!this._terminal || !this._engine) {
296
173
  throw new Error('Cannot use addon until it has been loaded');
297
174
  }
298
- const didOptionsChanged = this._lastSearchOptions ? this._didOptionsChange(this._lastSearchOptions, searchOptions) : true;
299
- this._lastSearchOptions = searchOptions;
300
- if (searchOptions?.decorations) {
301
- if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm || didOptionsChanged) {
302
- this._highlightAllMatches(term, searchOptions);
303
- }
175
+
176
+ this._state.lastSearchOptions = searchOptions;
177
+
178
+ if (this._state.shouldUpdateHighlighting(term, searchOptions)) {
179
+ this._highlightAllMatches(term, searchOptions!);
304
180
  }
305
181
 
306
182
  const found = this._findPreviousAndSelect(term, searchOptions, internalSearchOptions);
307
183
  this._fireResults(searchOptions);
308
- this._cachedSearchTerm = term;
184
+ this._state.cachedSearchTerm = term;
309
185
 
310
186
  return found;
311
187
  }
312
188
 
313
- private _didOptionsChange(lastSearchOptions: ISearchOptions, searchOptions?: ISearchOptions): boolean {
314
- if (!searchOptions) {
315
- return false;
316
- }
317
- if (lastSearchOptions.caseSensitive !== searchOptions.caseSensitive) {
318
- return true;
319
- }
320
- if (lastSearchOptions.regex !== searchOptions.regex) {
321
- return true;
322
- }
323
- if (lastSearchOptions.wholeWord !== searchOptions.wholeWord) {
324
- return true;
325
- }
326
- return false;
327
- }
328
-
329
189
  private _fireResults(searchOptions?: ISearchOptions): void {
330
- if (searchOptions?.decorations) {
331
- let resultIndex = -1;
332
- if (this._selectedDecoration.value) {
333
- const selectedMatch = this._selectedDecoration.value.match;
334
- for (let i = 0; i < this._searchResultsWithHighlight.length; i++) {
335
- const match = this._searchResultsWithHighlight[i];
336
- if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
337
- resultIndex = i;
338
- break;
339
- }
340
- }
341
- }
342
- this._onDidChangeResults.fire({ resultIndex, resultCount: this._searchResultsWithHighlight.length });
343
- }
190
+ this._resultTracker.fireResultsChanged(!!searchOptions?.decorations);
344
191
  }
345
192
 
346
193
  private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {
347
- if (!this._terminal) {
348
- throw new Error('Cannot use addon until it has been loaded');
194
+ if (!this._terminal || !this._engine) {
195
+ return false;
349
196
  }
350
- if (!this._terminal || !term || term.length === 0) {
351
- this._terminal?.clearSelection();
197
+ if (!this._state.isValidSearchTerm(term)) {
198
+ this._terminal.clearSelection();
352
199
  this.clearDecorations();
353
200
  return false;
354
201
  }
355
202
 
356
- const prevSelectedPos = this._terminal.getSelectionPosition();
357
- this._terminal.clearSelection();
358
-
359
- let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
360
- let startCol = this._terminal.cols;
361
- const isReverseSearch = true;
362
-
363
- this._lineCache.value!.initLinesCache();
364
- const searchPosition: ISearchPosition = {
365
- startRow,
366
- startCol
367
- };
368
-
369
- let result: ISearchResult | undefined;
370
- if (prevSelectedPos) {
371
- searchPosition.startRow = startRow = prevSelectedPos.start.y;
372
- searchPosition.startCol = startCol = prevSelectedPos.start.x;
373
- if (this._cachedSearchTerm !== term) {
374
- // Try to expand selection to right first.
375
- result = this._findInLine(term, searchPosition, searchOptions, false);
376
- if (!result) {
377
- // If selection was not able to be expanded to the right, then try reverse search
378
- searchPosition.startRow = startRow = prevSelectedPos.end.y;
379
- searchPosition.startCol = startCol = prevSelectedPos.end.x;
380
- }
381
- }
382
- }
383
-
384
- if (!result) {
385
- result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
386
- }
387
-
388
- // Search from startRow - 1 to top
389
- if (!result) {
390
- searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols);
391
- for (let y = startRow - 1; y >= 0; y--) {
392
- searchPosition.startRow = y;
393
- result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
394
- if (result) {
395
- break;
396
- }
397
- }
398
- }
399
- // If we hit the top and didn't search from the very bottom wrap back down
400
- if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {
401
- for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {
402
- searchPosition.startRow = y;
403
- result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
404
- if (result) {
405
- break;
406
- }
407
- }
408
- }
409
-
410
- // Set selection and scroll if a result was found
203
+ const result = this._engine.findPreviousWithSelection(term, searchOptions, this._state.cachedSearchTerm);
411
204
  return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);
412
205
  }
413
206
 
414
-
415
-
416
- /**
417
- * A found substring is a whole word if it doesn't have an alphanumeric character directly
418
- * adjacent to it.
419
- * @param searchIndex starting indext of the potential whole word substring
420
- * @param line entire string in which the potential whole word was found
421
- * @param term the substring that starts at searchIndex
422
- */
423
- private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
424
- return ((searchIndex === 0) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&
425
- (((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
426
- }
427
-
428
- /**
429
- * Searches a line for a search term. Takes the provided terminal line and searches the text line,
430
- * which may contain subsequent terminal lines if the text is wrapped. If the provided line number
431
- * is part of a wrapped text line that started on an earlier line then it is skipped since it will
432
- * be properly searched when the terminal line that the text starts on is searched.
433
- * @param term The search term.
434
- * @param searchPosition The position to start the search.
435
- * @param searchOptions Search options.
436
- * @param isReverseSearch Whether the search should start from the right side of the terminal and
437
- * search to the left.
438
- * @returns The search result if it was found.
439
- */
440
- protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
441
- const terminal = this._terminal!;
442
- const row = searchPosition.startRow;
443
- const col = searchPosition.startCol;
444
-
445
- // Ignore wrapped lines, only consider on unwrapped line (first row of command string).
446
- const firstLine = terminal.buffer.active.getLine(row);
447
- if (firstLine?.isWrapped) {
448
- if (isReverseSearch) {
449
- searchPosition.startCol += terminal.cols;
450
- return;
451
- }
452
-
453
- // This will iterate until we find the line start.
454
- // When we find it, we will search using the calculated start column.
455
- searchPosition.startRow--;
456
- searchPosition.startCol += terminal.cols;
457
- return this._findInLine(term, searchPosition, searchOptions);
458
- }
459
- let cache = this._lineCache.value!.getLineFromCache(row);
460
- if (!cache) {
461
- cache = this._lineCache.value!.translateBufferLineToStringWithWrap(row, true);
462
- this._lineCache.value!.setLineInCache(row, cache);
463
- }
464
- const [stringLine, offsets] = cache;
465
-
466
- const offset = this._bufferColsToStringOffset(row, col);
467
- let searchTerm = term;
468
- let searchStringLine = stringLine;
469
- if (!searchOptions.regex) {
470
- searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
471
- searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
472
- }
473
-
474
- let resultIndex = -1;
475
- if (searchOptions.regex) {
476
- const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');
477
- let foundTerm: RegExpExecArray | null;
478
- if (isReverseSearch) {
479
- // This loop will get the resultIndex of the _last_ regex match in the range 0..offset
480
- while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {
481
- resultIndex = searchRegex.lastIndex - foundTerm[0].length;
482
- term = foundTerm[0];
483
- searchRegex.lastIndex -= (term.length - 1);
484
- }
485
- } else {
486
- foundTerm = searchRegex.exec(searchStringLine.slice(offset));
487
- if (foundTerm && foundTerm[0].length > 0) {
488
- resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
489
- term = foundTerm[0];
490
- }
491
- }
492
- } else {
493
- if (isReverseSearch) {
494
- if (offset - searchTerm.length >= 0) {
495
- resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);
496
- }
497
- } else {
498
- resultIndex = searchStringLine.indexOf(searchTerm, offset);
499
- }
500
- }
501
-
502
- if (resultIndex >= 0) {
503
- if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
504
- return;
505
- }
506
-
507
- // Adjust the row number and search index if needed since a "line" of text can span multiple
508
- // rows
509
- let startRowOffset = 0;
510
- while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {
511
- startRowOffset++;
512
- }
513
- let endRowOffset = startRowOffset;
514
- while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {
515
- endRowOffset++;
516
- }
517
- const startColOffset = resultIndex - offsets[startRowOffset];
518
- const endColOffset = resultIndex + term.length - offsets[endRowOffset];
519
- const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);
520
- const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);
521
- const size = endColIndex - startColIndex + terminal.cols * (endRowOffset - startRowOffset);
522
-
523
- return {
524
- term,
525
- col: startColIndex,
526
- row: row + startRowOffset,
527
- size
528
- };
529
- }
530
- }
531
-
532
- private _stringLengthToBufferSize(row: number, offset: number): number {
533
- const line = this._terminal!.buffer.active.getLine(row);
534
- if (!line) {
535
- return 0;
536
- }
537
- for (let i = 0; i < offset; i++) {
538
- const cell = line.getCell(i);
539
- if (!cell) {
540
- break;
541
- }
542
- // Adjust the searchIndex to normalize emoji into single chars
543
- const char = cell.getChars();
544
- if (char.length > 1) {
545
- offset -= char.length - 1;
546
- }
547
- // Adjust the searchIndex for empty characters following wide unicode
548
- // chars (eg. CJK)
549
- const nextCell = line.getCell(i + 1);
550
- if (nextCell && nextCell.getWidth() === 0) {
551
- offset++;
552
- }
553
- }
554
- return offset;
555
- }
556
-
557
- private _bufferColsToStringOffset(startRow: number, cols: number): number {
558
- const terminal = this._terminal!;
559
- let lineIndex = startRow;
560
- let offset = 0;
561
- let line = terminal.buffer.active.getLine(lineIndex);
562
- while (cols > 0 && line) {
563
- for (let i = 0; i < cols && i < terminal.cols; i++) {
564
- const cell = line.getCell(i);
565
- if (!cell) {
566
- break;
567
- }
568
- if (cell.getWidth()) {
569
- // Treat null characters as whitespace to align with the translateToString API
570
- offset += cell.getCode() === 0 ? 1 : cell.getChars().length;
571
- }
572
- }
573
- lineIndex++;
574
- line = terminal.buffer.active.getLine(lineIndex);
575
- if (line && !line.isWrapped) {
576
- break;
577
- }
578
- cols -= terminal.cols;
579
- }
580
- return offset;
581
- }
582
-
583
-
584
-
585
207
  /**
586
208
  * Selects and scrolls to a result.
587
209
  * @param result The result to select.
588
210
  * @returns Whether a result was selected.
589
211
  */
590
- private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
591
- const terminal = this._terminal!;
592
- this._selectedDecoration.clear();
212
+ private _selectResult(result: ISearchResult | undefined, options?: any, noScroll?: boolean): boolean {
213
+ if (!this._terminal || !this._decorationManager) {
214
+ return false;
215
+ }
216
+
217
+ this._resultTracker.clearSelectedDecoration();
593
218
  if (!result) {
594
- terminal.clearSelection();
219
+ this._terminal.clearSelection();
595
220
  return false;
596
221
  }
597
- terminal.select(result.col, result.row, result.size);
222
+
223
+ this._terminal.select(result.col, result.row, result.size);
598
224
  if (options) {
599
- const decorations = this._createResultDecorations(result, options, true);
600
- if (decorations) {
601
- this._selectedDecoration.value = { decorations, match: result, dispose() { dispose(decorations); } };
225
+ const activeDecoration = this._decorationManager.createActiveDecoration(result, options);
226
+ if (activeDecoration) {
227
+ this._resultTracker.selectedDecoration = activeDecoration;
602
228
  }
603
229
  }
604
230
 
605
231
  if (!noScroll) {
606
232
  // If it is not in the viewport then we scroll else it just gets selected
607
- if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) {
608
- let scroll = result.row - terminal.buffer.active.viewportY;
609
- scroll -= Math.floor(terminal.rows / 2);
610
- terminal.scrollLines(scroll);
233
+ if (result.row >= (this._terminal.buffer.active.viewportY + this._terminal.rows) || result.row < this._terminal.buffer.active.viewportY) {
234
+ let scroll = result.row - this._terminal.buffer.active.viewportY;
235
+ scroll -= Math.floor(this._terminal.rows / 2);
236
+ this._terminal.scrollLines(scroll);
611
237
  }
612
238
  }
613
239
  return true;
614
240
  }
615
-
616
- /**
617
- * Applies styles to the decoration when it is rendered.
618
- * @param element The decoration's element.
619
- * @param borderColor The border color to apply.
620
- * @param isActiveResult Whether the element is part of the active search result.
621
- * @returns
622
- */
623
- private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
624
- if (!element.classList.contains('xterm-find-result-decoration')) {
625
- element.classList.add('xterm-find-result-decoration');
626
- if (borderColor) {
627
- element.style.outline = `1px solid ${borderColor}`;
628
- }
629
- }
630
- if (isActiveResult) {
631
- element.classList.add('xterm-find-active-result-decoration');
632
- }
633
- }
634
-
635
- /**
636
- * Creates a decoration for the result and applies styles
637
- * @param result the search result for which to create the decoration
638
- * @param options the options for the decoration
639
- * @returns the {@link IDecoration} or undefined if the marker has already been disposed of
640
- */
641
- private _createResultDecorations(result: ISearchResult, options: ISearchDecorationOptions, isActiveResult: boolean): IDecoration[] | undefined {
642
- const terminal = this._terminal!;
643
-
644
- // Gather decoration ranges for this match as it could wrap
645
- const decorationRanges: [number, number, number][] = [];
646
- let currentCol = result.col;
647
- let remainingSize = result.size;
648
- let markerOffset = -terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row;
649
- while (remainingSize > 0) {
650
- const amountThisRow = Math.min(terminal.cols - currentCol, remainingSize);
651
- decorationRanges.push([markerOffset, currentCol, amountThisRow]);
652
- currentCol = 0;
653
- remainingSize -= amountThisRow;
654
- markerOffset++;
655
- }
656
-
657
- // Create the decorations
658
- const decorations: IDecoration[] = [];
659
- for (const range of decorationRanges) {
660
- const marker = terminal.registerMarker(range[0]);
661
- const decoration = terminal.registerDecoration({
662
- marker,
663
- x: range[1],
664
- width: range[2],
665
- backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,
666
- overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
667
- color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,
668
- position: 'center'
669
- }
670
- });
671
- if (decoration) {
672
- const disposables: IDisposable[] = [];
673
- disposables.push(marker);
674
- disposables.push(decoration.onRender((e) => this._applyStyles(e, isActiveResult ? options.activeMatchBorder : options.matchBorder, false)));
675
- disposables.push(decoration.onDispose(() => dispose(disposables)));
676
- decorations.push(decoration);
677
- }
678
- }
679
-
680
- return decorations.length === 0 ? undefined : decorations;
681
- }
682
241
  }