@xterm/addon-search 0.16.0-beta.98 → 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.
- package/lib/addon-search.js +1 -1
- package/lib/addon-search.js.map +1 -1
- package/lib/addon-search.mjs +13 -13
- package/lib/addon-search.mjs.map +4 -4
- package/package.json +2 -5
- package/src/DecorationManager.ts +157 -0
- package/src/SearchAddon.ts +110 -620
- package/src/SearchEngine.ts +394 -0
- package/src/SearchLineCache.ts +114 -0
- package/src/SearchResultTracker.ts +119 -0
- package/src/SearchState.ts +106 -0
- package/typings/addon-search.d.ts +16 -2
|
@@ -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
|
+
}
|