@xterm/addon-search 0.16.0-beta.99 → 0.16.0

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.
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import type { Terminal } from '@xterm/xterm';
7
+ import type { ISearchOptions } from '@xterm/addon-search';
8
+ import type { SearchLineCache } from './SearchLineCache';
9
+
10
+ /**
11
+ * Represents the position to start a search from.
12
+ */
13
+ interface ISearchPosition {
14
+ startCol: number;
15
+ startRow: number;
16
+ }
17
+
18
+ /**
19
+ * Represents a search result with its position and content.
20
+ */
21
+ export interface ISearchResult {
22
+ term: string;
23
+ col: number;
24
+ row: number;
25
+ size: number;
26
+ }
27
+
28
+ /**
29
+ * Configuration constants for the search engine functionality.
30
+ */
31
+ const enum Constants {
32
+ /**
33
+ * Characters that are considered non-word characters for search boundary detection. These
34
+ * characters are used to determine word boundaries when performing whole-word searches. Includes
35
+ * common punctuation, symbols, and whitespace characters.
36
+ */
37
+ NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?'
38
+ }
39
+
40
+ /**
41
+ * Core search engine that handles finding text within terminal content.
42
+ * This class is responsible for the actual search algorithms and position calculations.
43
+ */
44
+ export class SearchEngine {
45
+ constructor(
46
+ private readonly _terminal: Terminal,
47
+ private readonly _lineCache: SearchLineCache
48
+ ) {}
49
+
50
+ /**
51
+ * Find the first occurrence of a term starting from a specific position.
52
+ * @param term The search term.
53
+ * @param startRow The row to start searching from.
54
+ * @param startCol The column to start searching from.
55
+ * @param searchOptions Search options.
56
+ * @returns The search result if found, undefined otherwise.
57
+ */
58
+ public find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
59
+ if (!term || term.length === 0) {
60
+ this._terminal.clearSelection();
61
+ return undefined;
62
+ }
63
+ if (startCol > this._terminal.cols) {
64
+ throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
65
+ }
66
+
67
+ this._lineCache.initLinesCache();
68
+
69
+ const searchPosition: ISearchPosition = {
70
+ startRow,
71
+ startCol
72
+ };
73
+
74
+ // Search startRow
75
+ let result = this._findInLine(term, searchPosition, searchOptions);
76
+ // Search from startRow + 1 to end
77
+ if (!result) {
78
+ for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
79
+ searchPosition.startRow = y;
80
+ searchPosition.startCol = 0;
81
+ result = this._findInLine(term, searchPosition, searchOptions);
82
+ if (result) {
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ return result;
88
+ }
89
+
90
+ /**
91
+ * Find the next occurrence of a term with wrapping and selection management.
92
+ * @param term The search term.
93
+ * @param searchOptions Search options.
94
+ * @param cachedSearchTerm The cached search term to determine incremental behavior.
95
+ * @returns The search result if found, undefined otherwise.
96
+ */
97
+ public findNextWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {
98
+ if (!term || term.length === 0) {
99
+ this._terminal.clearSelection();
100
+ return undefined;
101
+ }
102
+
103
+ const prevSelectedPos = this._terminal.getSelectionPosition();
104
+ this._terminal.clearSelection();
105
+
106
+ let startCol = 0;
107
+ let startRow = 0;
108
+ if (prevSelectedPos) {
109
+ if (cachedSearchTerm === term) {
110
+ startCol = prevSelectedPos.end.x;
111
+ startRow = prevSelectedPos.end.y;
112
+ } else {
113
+ startCol = prevSelectedPos.start.x;
114
+ startRow = prevSelectedPos.start.y;
115
+ }
116
+ }
117
+
118
+ this._lineCache.initLinesCache();
119
+
120
+ const searchPosition: ISearchPosition = {
121
+ startRow,
122
+ startCol
123
+ };
124
+
125
+ // Search startRow
126
+ let result = this._findInLine(term, searchPosition, searchOptions);
127
+ // Search from startRow + 1 to end
128
+ if (!result) {
129
+ for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
130
+ searchPosition.startRow = y;
131
+ searchPosition.startCol = 0;
132
+ result = this._findInLine(term, searchPosition, searchOptions);
133
+ if (result) {
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ // If we hit the bottom and didn't search from the very top wrap back up
139
+ if (!result && startRow !== 0) {
140
+ for (let y = 0; y < startRow; y++) {
141
+ searchPosition.startRow = y;
142
+ searchPosition.startCol = 0;
143
+ result = this._findInLine(term, searchPosition, searchOptions);
144
+ if (result) {
145
+ break;
146
+ }
147
+ }
148
+ }
149
+
150
+ // If there is only one result, wrap back and return selection if it exists.
151
+ if (!result && prevSelectedPos) {
152
+ searchPosition.startRow = prevSelectedPos.start.y;
153
+ searchPosition.startCol = 0;
154
+ result = this._findInLine(term, searchPosition, searchOptions);
155
+ }
156
+
157
+ return result;
158
+ }
159
+
160
+ /**
161
+ * Find the previous occurrence of a term with wrapping and selection management.
162
+ * @param term The search term.
163
+ * @param searchOptions Search options.
164
+ * @param cachedSearchTerm The cached search term to determine if expansion should occur.
165
+ * @returns The search result if found, undefined otherwise.
166
+ */
167
+ public findPreviousWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {
168
+ if (!term || term.length === 0) {
169
+ this._terminal.clearSelection();
170
+ return undefined;
171
+ }
172
+
173
+ const prevSelectedPos = this._terminal.getSelectionPosition();
174
+ this._terminal.clearSelection();
175
+
176
+ let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
177
+ let startCol = this._terminal.cols;
178
+ const isReverseSearch = true;
179
+
180
+ this._lineCache.initLinesCache();
181
+ const searchPosition: ISearchPosition = {
182
+ startRow,
183
+ startCol
184
+ };
185
+
186
+ let result: ISearchResult | undefined;
187
+ if (prevSelectedPos) {
188
+ searchPosition.startRow = startRow = prevSelectedPos.start.y;
189
+ searchPosition.startCol = startCol = prevSelectedPos.start.x;
190
+ if (cachedSearchTerm !== term) {
191
+ // Try to expand selection to right first.
192
+ result = this._findInLine(term, searchPosition, searchOptions, false);
193
+ if (!result) {
194
+ // If selection was not able to be expanded to the right, then try reverse search
195
+ searchPosition.startRow = startRow = prevSelectedPos.end.y;
196
+ searchPosition.startCol = startCol = prevSelectedPos.end.x;
197
+ }
198
+ }
199
+ }
200
+
201
+ if (!result) {
202
+ result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
203
+ }
204
+
205
+ // Search from startRow - 1 to top
206
+ if (!result) {
207
+ searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols);
208
+ for (let y = startRow - 1; y >= 0; y--) {
209
+ searchPosition.startRow = y;
210
+ result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
211
+ if (result) {
212
+ break;
213
+ }
214
+ }
215
+ }
216
+ // If we hit the top and didn't search from the very bottom wrap back down
217
+ if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {
218
+ for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {
219
+ searchPosition.startRow = y;
220
+ result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
221
+ if (result) {
222
+ break;
223
+ }
224
+ }
225
+ }
226
+
227
+ return result;
228
+ }
229
+
230
+ /**
231
+ * A found substring is a whole word if it doesn't have an alphanumeric character directly
232
+ * adjacent to it.
233
+ * @param searchIndex starting index of the potential whole word substring
234
+ * @param line entire string in which the potential whole word was found
235
+ * @param term the substring that starts at searchIndex
236
+ */
237
+ private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
238
+ return ((searchIndex === 0) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&
239
+ (((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
240
+ }
241
+
242
+ /**
243
+ * Searches a line for a search term. Takes the provided terminal line and searches the text line,
244
+ * which may contain subsequent terminal lines if the text is wrapped. If the provided line number
245
+ * is part of a wrapped text line that started on an earlier line then it is skipped since it will
246
+ * be properly searched when the terminal line that the text starts on is searched.
247
+ * @param term The search term.
248
+ * @param searchPosition The position to start the search.
249
+ * @param searchOptions Search options.
250
+ * @param isReverseSearch Whether the search should start from the right side of the terminal and
251
+ * search to the left.
252
+ * @returns The search result if it was found.
253
+ */
254
+ private _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
255
+ const row = searchPosition.startRow;
256
+ const col = searchPosition.startCol;
257
+
258
+ // Ignore wrapped lines, only consider on unwrapped line (first row of command string).
259
+ const firstLine = this._terminal.buffer.active.getLine(row);
260
+ if (firstLine?.isWrapped) {
261
+ if (isReverseSearch) {
262
+ searchPosition.startCol += this._terminal.cols;
263
+ return;
264
+ }
265
+
266
+ // This will iterate until we find the line start.
267
+ // When we find it, we will search using the calculated start column.
268
+ searchPosition.startRow--;
269
+ searchPosition.startCol += this._terminal.cols;
270
+ return this._findInLine(term, searchPosition, searchOptions);
271
+ }
272
+ let cache = this._lineCache.getLineFromCache(row);
273
+ if (!cache) {
274
+ cache = this._lineCache.translateBufferLineToStringWithWrap(row, true);
275
+ this._lineCache.setLineInCache(row, cache);
276
+ }
277
+ const [stringLine, offsets] = cache;
278
+
279
+ const offset = this._bufferColsToStringOffset(row, col);
280
+ let searchTerm = term;
281
+ let searchStringLine = stringLine;
282
+ if (!searchOptions.regex) {
283
+ searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
284
+ searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
285
+ }
286
+
287
+ let resultIndex = -1;
288
+ if (searchOptions.regex) {
289
+ const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');
290
+ let foundTerm: RegExpExecArray | null;
291
+ if (isReverseSearch) {
292
+ // This loop will get the resultIndex of the _last_ regex match in the range 0..offset
293
+ while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {
294
+ resultIndex = searchRegex.lastIndex - foundTerm[0].length;
295
+ term = foundTerm[0];
296
+ searchRegex.lastIndex -= (term.length - 1);
297
+ }
298
+ } else {
299
+ foundTerm = searchRegex.exec(searchStringLine.slice(offset));
300
+ if (foundTerm && foundTerm[0].length > 0) {
301
+ resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
302
+ term = foundTerm[0];
303
+ }
304
+ }
305
+ } else {
306
+ if (isReverseSearch) {
307
+ if (offset - searchTerm.length >= 0) {
308
+ resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);
309
+ }
310
+ } else {
311
+ resultIndex = searchStringLine.indexOf(searchTerm, offset);
312
+ }
313
+ }
314
+
315
+ if (resultIndex >= 0) {
316
+ if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
317
+ return;
318
+ }
319
+
320
+ // Adjust the row number and search index if needed since a "line" of text can span multiple
321
+ // rows
322
+ let startRowOffset = 0;
323
+ while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {
324
+ startRowOffset++;
325
+ }
326
+ let endRowOffset = startRowOffset;
327
+ while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {
328
+ endRowOffset++;
329
+ }
330
+ const startColOffset = resultIndex - offsets[startRowOffset];
331
+ const endColOffset = resultIndex + term.length - offsets[endRowOffset];
332
+ const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);
333
+ const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);
334
+ const size = endColIndex - startColIndex + this._terminal.cols * (endRowOffset - startRowOffset);
335
+
336
+ return {
337
+ term,
338
+ col: startColIndex,
339
+ row: row + startRowOffset,
340
+ size
341
+ };
342
+ }
343
+ }
344
+
345
+ private _stringLengthToBufferSize(row: number, offset: number): number {
346
+ const line = this._terminal.buffer.active.getLine(row);
347
+ if (!line) {
348
+ return 0;
349
+ }
350
+ for (let i = 0; i < offset; i++) {
351
+ const cell = line.getCell(i);
352
+ if (!cell) {
353
+ break;
354
+ }
355
+ // Adjust the searchIndex to normalize emoji into single chars
356
+ const char = cell.getChars();
357
+ if (char.length > 1) {
358
+ offset -= char.length - 1;
359
+ }
360
+ // Adjust the searchIndex for empty characters following wide unicode
361
+ // chars (eg. CJK)
362
+ const nextCell = line.getCell(i + 1);
363
+ if (nextCell && nextCell.getWidth() === 0) {
364
+ offset++;
365
+ }
366
+ }
367
+ return offset;
368
+ }
369
+
370
+ private _bufferColsToStringOffset(startRow: number, cols: number): number {
371
+ let lineIndex = startRow;
372
+ let offset = 0;
373
+ let line = this._terminal.buffer.active.getLine(lineIndex);
374
+ while (cols > 0 && line) {
375
+ for (let i = 0; i < cols && i < this._terminal.cols; i++) {
376
+ const cell = line.getCell(i);
377
+ if (!cell) {
378
+ break;
379
+ }
380
+ if (cell.getWidth()) {
381
+ // Treat null characters as whitespace to align with the translateToString API
382
+ offset += cell.getCode() === 0 ? 1 : cell.getChars().length;
383
+ }
384
+ }
385
+ lineIndex++;
386
+ line = this._terminal.buffer.active.getLine(lineIndex);
387
+ if (line && !line.isWrapped) {
388
+ break;
389
+ }
390
+ cols -= this._terminal.cols;
391
+ }
392
+ return offset;
393
+ }
394
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import type { Terminal } from '@xterm/xterm';
7
+ import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
8
+ import { disposableTimeout } from 'vs/base/common/async';
9
+
10
+ export type LineCacheEntry = [
11
+ /**
12
+ * The string representation of a line (as opposed to the buffer cell representation).
13
+ */
14
+ lineAsString: string,
15
+ /**
16
+ * The offsets where each line starts when the entry describes a wrapped line.
17
+ */
18
+ lineOffsets: number[]
19
+ ];
20
+
21
+ /**
22
+ * Configuration constants for the search line cache functionality.
23
+ */
24
+ const enum Constants {
25
+ /**
26
+ * Time-to-live for cached search results in milliseconds. After this duration, cached search
27
+ * results will be invalidated to ensure they remain consistent with terminal content changes.
28
+ */
29
+ LINES_CACHE_TIME_TO_LIVE = 15000
30
+ }
31
+
32
+ export class SearchLineCache extends Disposable {
33
+ /**
34
+ * translateBufferLineToStringWithWrap is a fairly expensive call.
35
+ * We memoize the calls into an array that has a time based ttl.
36
+ * _linesCache is also invalidated when the terminal cursor moves.
37
+ */
38
+ private _linesCache: LineCacheEntry[] | undefined;
39
+ private _linesCacheTimeout = this._register(new MutableDisposable());
40
+ private _linesCacheDisposables = this._register(new MutableDisposable());
41
+
42
+ constructor(private readonly _terminal: Terminal) {
43
+ super();
44
+ this._register(toDisposable(() => this._destroyLinesCache()));
45
+ }
46
+
47
+ /**
48
+ * Sets up a line cache with a ttl
49
+ */
50
+ public initLinesCache(): void {
51
+ if (!this._linesCache) {
52
+ this._linesCache = new Array(this._terminal.buffer.active.length);
53
+ this._linesCacheDisposables.value = combinedDisposable(
54
+ this._terminal.onLineFeed(() => this._destroyLinesCache()),
55
+ this._terminal.onCursorMove(() => this._destroyLinesCache()),
56
+ this._terminal.onResize(() => this._destroyLinesCache())
57
+ );
58
+ }
59
+
60
+ this._linesCacheTimeout.value = disposableTimeout(() => this._destroyLinesCache(), Constants.LINES_CACHE_TIME_TO_LIVE);
61
+ }
62
+
63
+ private _destroyLinesCache(): void {
64
+ this._linesCache = undefined;
65
+ this._linesCacheDisposables.clear();
66
+ this._linesCacheTimeout.clear();
67
+ }
68
+
69
+ public getLineFromCache(row: number): LineCacheEntry | undefined {
70
+ return this._linesCache?.[row];
71
+ }
72
+
73
+ public setLineInCache(row: number, entry: LineCacheEntry): void {
74
+ if (this._linesCache) {
75
+ this._linesCache[row] = entry;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Translates a buffer line to a string, including subsequent lines if they are wraps.
81
+ * Wide characters will count as two columns in the resulting string. This
82
+ * function is useful for getting the actual text underneath the raw selection
83
+ * position.
84
+ * @param lineIndex The index of the line being translated.
85
+ * @param trimRight Whether to trim whitespace to the right.
86
+ */
87
+ public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
88
+ const strings = [];
89
+ const lineOffsets = [0];
90
+ let line = this._terminal.buffer.active.getLine(lineIndex);
91
+ while (line) {
92
+ const nextLine = this._terminal.buffer.active.getLine(lineIndex + 1);
93
+ const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
94
+ let string = line.translateToString(!lineWrapsToNext && trimRight);
95
+ if (lineWrapsToNext && nextLine) {
96
+ const lastCell = line.getCell(line.length - 1);
97
+ const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;
98
+ // a wide character wrapped to the next line
99
+ if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {
100
+ string = string.slice(0, -1);
101
+ }
102
+ }
103
+ strings.push(string);
104
+ if (lineWrapsToNext) {
105
+ lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);
106
+ } else {
107
+ break;
108
+ }
109
+ lineIndex++;
110
+ line = nextLine;
111
+ }
112
+ return [strings.join(''), lineOffsets];
113
+ }
114
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import type { ISearchResultChangeEvent } from '@xterm/addon-search';
7
+ import type { IDisposable } from '@xterm/xterm';
8
+ import { Emitter, Event } from 'vs/base/common/event';
9
+ import { Disposable } from 'vs/base/common/lifecycle';
10
+ import type { ISearchResult } from './SearchEngine';
11
+
12
+ /**
13
+ * Interface for managing a currently selected decoration.
14
+ */
15
+ interface ISelectedDecoration extends IDisposable {
16
+ match: ISearchResult;
17
+ }
18
+
19
+ /**
20
+ * Tracks search results, manages result indexing, and fires events when results change.
21
+ * This class provides centralized management of search result state and notifications.
22
+ */
23
+ export class SearchResultTracker extends Disposable {
24
+ private _searchResults: ISearchResult[] = [];
25
+ private _selectedDecoration: ISelectedDecoration | undefined;
26
+
27
+ private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
28
+ public get onDidChangeResults(): Event<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
29
+
30
+ /**
31
+ * Gets the current search results.
32
+ */
33
+ public get searchResults(): ReadonlyArray<ISearchResult> {
34
+ return this._searchResults;
35
+ }
36
+
37
+ /**
38
+ * Gets the currently selected decoration.
39
+ */
40
+ public get selectedDecoration(): ISelectedDecoration | undefined {
41
+ return this._selectedDecoration;
42
+ }
43
+
44
+ /**
45
+ * Sets the currently selected decoration.
46
+ */
47
+ public set selectedDecoration(decoration: ISelectedDecoration | undefined) {
48
+ this._selectedDecoration = decoration;
49
+ }
50
+
51
+ /**
52
+ * Updates the search results with a new set of results.
53
+ * @param results The new search results.
54
+ * @param maxResults The maximum number of results to track.
55
+ */
56
+ public updateResults(results: ISearchResult[], maxResults: number): void {
57
+ this._searchResults = results.slice(0, maxResults);
58
+ }
59
+
60
+ /**
61
+ * Clears all search results.
62
+ */
63
+ public clearResults(): void {
64
+ this._searchResults = [];
65
+ }
66
+
67
+ /**
68
+ * Clears the selected decoration.
69
+ */
70
+ public clearSelectedDecoration(): void {
71
+ if (this._selectedDecoration) {
72
+ this._selectedDecoration.dispose();
73
+ this._selectedDecoration = undefined;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Finds the index of a result in the current results array.
79
+ * @param result The result to find.
80
+ * @returns The index of the result, or -1 if not found.
81
+ */
82
+ public findResultIndex(result: ISearchResult): number {
83
+ for (let i = 0; i < this._searchResults.length; i++) {
84
+ const match = this._searchResults[i];
85
+ if (match.row === result.row && match.col === result.col && match.size === result.size) {
86
+ return i;
87
+ }
88
+ }
89
+ return -1;
90
+ }
91
+
92
+ /**
93
+ * Fires a result change event with the current state.
94
+ * @param hasDecorations Whether decorations are enabled.
95
+ */
96
+ public fireResultsChanged(hasDecorations: boolean): void {
97
+ if (!hasDecorations) {
98
+ return;
99
+ }
100
+
101
+ let resultIndex = -1;
102
+ if (this._selectedDecoration) {
103
+ resultIndex = this.findResultIndex(this._selectedDecoration.match);
104
+ }
105
+
106
+ this._onDidChangeResults.fire({
107
+ resultIndex,
108
+ resultCount: this._searchResults.length
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Resets all state.
114
+ */
115
+ public reset(): void {
116
+ this.clearSelectedDecoration();
117
+ this.clearResults();
118
+ }
119
+ }