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