@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.
- package/lib/addon-search.js +1 -1
- package/lib/addon-search.js.map +1 -1
- package/lib/addon-search.mjs +12 -12
- package/lib/addon-search.mjs.map +4 -4
- package/package.json +3 -3
- package/src/DecorationManager.ts +157 -0
- package/src/SearchAddon.ts +82 -523
- package/src/SearchEngine.ts +394 -0
- package/src/SearchLineCache.ts +1 -1
- package/src/SearchResultTracker.ts +119 -0
- package/src/SearchState.ts +106 -0
package/src/SearchAddon.ts
CHANGED
|
@@ -3,50 +3,25 @@
|
|
|
3
3
|
* @license MIT
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Terminal, IDisposable, ITerminalAddon
|
|
7
|
-
import type { SearchAddon as ISearchApi, ISearchOptions,
|
|
8
|
-
import {
|
|
9
|
-
import { Disposable,
|
|
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
|
-
|
|
71
|
-
|
|
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.
|
|
67
|
+
if (this._state.cachedSearchTerm && this._state.lastSearchOptions?.decorations) {
|
|
90
68
|
this._highlightTimeout.value = disposableTimeout(() => {
|
|
91
|
-
const term = this.
|
|
92
|
-
this.
|
|
93
|
-
this.findPrevious(term!, { ...this.
|
|
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.
|
|
100
|
-
|
|
101
|
-
this.
|
|
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.
|
|
81
|
+
this._state.clearCachedTerm();
|
|
106
82
|
}
|
|
107
83
|
}
|
|
108
84
|
|
|
109
85
|
public clearActiveDecoration(): void {
|
|
110
|
-
this.
|
|
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
|
-
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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 (
|
|
131
|
+
if (results.length >= this._highlightLimit) {
|
|
156
132
|
break;
|
|
157
133
|
}
|
|
158
134
|
prevResult = result;
|
|
159
|
-
|
|
160
|
-
result = this.
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 || !
|
|
222
|
-
this._terminal?.clearSelection();
|
|
223
|
-
this.clearDecorations();
|
|
151
|
+
if (!this._terminal || !this._engine) {
|
|
224
152
|
return false;
|
|
225
153
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
this.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
194
|
+
if (!this._terminal || !this._engine) {
|
|
195
|
+
return false;
|
|
349
196
|
}
|
|
350
|
-
if (!this.
|
|
351
|
-
this._terminal
|
|
197
|
+
if (!this._state.isValidSearchTerm(term)) {
|
|
198
|
+
this._terminal.clearSelection();
|
|
352
199
|
this.clearDecorations();
|
|
353
200
|
return false;
|
|
354
201
|
}
|
|
355
202
|
|
|
356
|
-
const
|
|
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?:
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
219
|
+
this._terminal.clearSelection();
|
|
595
220
|
return false;
|
|
596
221
|
}
|
|
597
|
-
|
|
222
|
+
|
|
223
|
+
this._terminal.select(result.col, result.row, result.size);
|
|
598
224
|
if (options) {
|
|
599
|
-
const
|
|
600
|
-
if (
|
|
601
|
-
this.
|
|
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 >= (
|
|
608
|
-
let scroll = result.row -
|
|
609
|
-
scroll -= Math.floor(
|
|
610
|
-
|
|
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
|
}
|