@adecore/editor 0.22.0 → 0.23.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/dist/attribution.d.ts +9 -8
- package/dist/attribution.js +44 -9
- package/dist/bidi.d.ts +44 -0
- package/dist/bidi.js +26 -0
- package/dist/column-selection.js +2 -1
- package/dist/controller.d.ts +6 -1
- package/dist/controller.js +93 -3
- package/dist/editor.css +81 -1
- package/dist/engine.js +184 -41
- package/dist/fake.d.ts +43 -5
- package/dist/fake.js +117 -13
- package/dist/find-search.d.ts +26 -0
- package/dist/find-search.js +130 -0
- package/dist/find.d.ts +50 -2
- package/dist/find.js +575 -25
- package/dist/geometry-stops.d.ts +6 -0
- package/dist/geometry-stops.js +47 -0
- package/dist/grammar-preparation.d.ts +29 -0
- package/dist/grammar-preparation.js +147 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/keymap-table.d.ts +10 -1
- package/dist/keymap-table.js +37 -1
- package/dist/keymap.d.ts +1 -0
- package/dist/keymap.js +4 -1
- package/dist/layout-changes.d.ts +6 -0
- package/dist/layout-changes.js +27 -0
- package/dist/layout.d.ts +45 -2
- package/dist/layout.js +202 -58
- package/dist/offsets.d.ts +1 -0
- package/dist/offsets.js +23 -0
- package/dist/outline.js +5 -4
- package/dist/paint.d.ts +11 -3
- package/dist/paint.js +180 -34
- package/dist/search-client.d.ts +27 -0
- package/dist/search-client.js +210 -0
- package/dist/search-protocol.d.ts +103 -0
- package/dist/search-protocol.js +3 -0
- package/dist/search-worker.d.ts +11 -0
- package/dist/search-worker.js +137 -0
- package/dist/semantic.d.ts +2 -1
- package/dist/semantic.js +6 -1
- package/dist/shiki-document.d.ts +8 -0
- package/dist/shiki-document.js +93 -0
- package/dist/shiki-worker.d.ts +65 -0
- package/dist/shiki-worker.js +265 -0
- package/dist/shiki.d.ts +1 -0
- package/dist/shiki.js +96 -33
- package/dist/testing.d.ts +1 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.js +37 -7
- package/dist/touch.d.ts +27 -0
- package/dist/touch.js +100 -0
- package/dist/types.d.ts +84 -2
- package/dist/uniform-geometry.d.ts +7 -0
- package/dist/uniform-geometry.js +49 -0
- package/dist/view.d.ts +68 -17
- package/dist/view.js +424 -97
- package/dist/worker-client.d.ts +37 -0
- package/dist/worker-client.js +195 -0
- package/package.json +17 -2
- package/src/attribution.ts +50 -14
- package/src/bidi.ts +32 -0
- package/src/column-selection.ts +2 -1
- package/src/controller.ts +97 -3
- package/src/editor.css +81 -1
- package/src/engine.ts +216 -42
- package/src/fake.ts +152 -14
- package/src/find-search.ts +165 -0
- package/src/find.ts +622 -21
- package/src/geometry-stops.ts +51 -0
- package/src/grammar-preparation.ts +158 -0
- package/src/index.ts +10 -0
- package/src/keymap-table.ts +44 -2
- package/src/keymap.ts +5 -1
- package/src/layout-changes.ts +33 -0
- package/src/layout.ts +262 -63
- package/src/offsets.ts +24 -0
- package/src/outline.ts +5 -4
- package/src/paint.ts +202 -36
- package/src/search-client.ts +306 -0
- package/src/search-protocol.ts +100 -0
- package/src/search-worker.ts +171 -0
- package/src/semantic.ts +8 -2
- package/src/shiki-document.ts +143 -0
- package/src/shiki-worker.ts +344 -0
- package/src/shiki.ts +103 -33
- package/src/testing.ts +2 -0
- package/src/tokens.ts +36 -7
- package/src/touch.ts +134 -0
- package/src/types.ts +174 -8
- package/src/uniform-geometry.ts +57 -0
- package/src/view.ts +500 -102
- package/src/worker-client.ts +212 -0
package/src/find.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type { DocumentChange, DocumentModel } from '@adecore/editor-core';
|
|
1
|
+
import type { DocumentChange, DocumentModel, FindMatch, TextEdit } from '@adecore/editor-core';
|
|
2
2
|
import { mapOffset } from './offsets.ts';
|
|
3
|
+
import { FindSearchQueue, scheduleFind, type FindSearchOptions } from './find-search.ts';
|
|
4
|
+
import { EditorSearchError } from './search-client.ts';
|
|
5
|
+
import type { EditorSearchSnapshot } from './search-protocol.ts';
|
|
6
|
+
import { SEARCH_LIMIT, MAX_REPLACEMENT_PAGE, MAX_REPLACEMENT_CHARACTERS } from './search-protocol.ts';
|
|
3
7
|
import type { EditorFindQuery, EditorFindState } from './types.ts';
|
|
4
8
|
|
|
5
9
|
export interface FindMark {
|
|
@@ -7,8 +11,12 @@ export interface FindMark {
|
|
|
7
11
|
to: number;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
export const FIND_LIMIT = SEARCH_LIMIT;
|
|
15
|
+
const LIVE_REGEX_LIMIT = 8_000_000;
|
|
16
|
+
|
|
17
|
+
export interface FindControllerOptions extends FindSearchOptions {
|
|
18
|
+
readonly changed: (reveal?: boolean) => void;
|
|
19
|
+
}
|
|
12
20
|
|
|
13
21
|
/*
|
|
14
22
|
* The find of the host's find bar: every match marked, one of them current. It searches the model on
|
|
@@ -16,17 +24,42 @@ export const FIND_LIMIT = 10000;
|
|
|
16
24
|
*/
|
|
17
25
|
export class FindController {
|
|
18
26
|
private query: EditorFindQuery | null = null;
|
|
19
|
-
private marks: FindMark[] = [];
|
|
27
|
+
private marks: readonly FindMark[] = [];
|
|
28
|
+
private cache?: { revision: number; key: string; matches: FindMatch[]; complete: boolean };
|
|
20
29
|
private index: number | null = null;
|
|
30
|
+
private outside: FindMark | null = null;
|
|
31
|
+
private seeking = false;
|
|
32
|
+
private replacing = 0;
|
|
33
|
+
private replaceEpoch = -1;
|
|
34
|
+
private replaced: number | undefined;
|
|
35
|
+
private replaceFailed: EditorFindState['replaceFailed'];
|
|
21
36
|
/* The text a search in the selection is held to, which follows the text through edits; null when nothing was selected. */
|
|
22
37
|
private bounds: { from: number; to: number } | null = null;
|
|
23
38
|
/* Where the next refresh looks for the current match from, once: right after what a replace wrote. */
|
|
24
39
|
pendingAnchor: number | undefined;
|
|
25
40
|
|
|
26
41
|
private readonly model: DocumentModel;
|
|
42
|
+
private readonly options: FindControllerOptions | undefined;
|
|
43
|
+
private readonly search: FindSearchQueue | undefined;
|
|
44
|
+
private regexCache: { revision: number; key: string; complete: boolean } | undefined;
|
|
45
|
+
private snapshotMemo: EditorSearchSnapshot | undefined;
|
|
46
|
+
private retained: FindMatch | null = null;
|
|
47
|
+
private epoch = 0;
|
|
48
|
+
private searching = false;
|
|
49
|
+
private failed: EditorFindState['failed'];
|
|
50
|
+
private captureFailed: EditorFindState['captureFailed'];
|
|
51
|
+
private pendingStep: 1 | -1 | undefined;
|
|
52
|
+
private cancelSearch: (() => void) | undefined;
|
|
53
|
+
private cancelMatch: (() => void) | undefined;
|
|
54
|
+
private preview = false;
|
|
55
|
+
private disposed = false;
|
|
56
|
+
private revision = -1;
|
|
57
|
+
private cursorRequests = 0;
|
|
27
58
|
|
|
28
|
-
constructor(model: DocumentModel) {
|
|
59
|
+
constructor(model: DocumentModel, options?: FindControllerOptions) {
|
|
29
60
|
this.model = model;
|
|
61
|
+
this.options = options;
|
|
62
|
+
this.search = options === undefined ? undefined : new FindSearchQueue(options);
|
|
30
63
|
}
|
|
31
64
|
|
|
32
65
|
get activeQuery(): EditorFindQuery | null {
|
|
@@ -46,7 +79,18 @@ export class FindController {
|
|
|
46
79
|
}
|
|
47
80
|
|
|
48
81
|
get state(): EditorFindState {
|
|
49
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
count: this.marks.length,
|
|
84
|
+
current: this.index,
|
|
85
|
+
...(this.cache?.complete === false || (this.failed === undefined && this.regexCache?.complete === false) ? { limited: true } : {}),
|
|
86
|
+
...(this.searching || this.seeking || (this.replacing > 0 && this.replaceEpoch === this.epoch) ? { searching: true } : {}),
|
|
87
|
+
...(this.replaced === undefined ? {} : { replaced: this.replaced }),
|
|
88
|
+
...(this.replaceFailed === undefined ? {} : { replaceFailed: this.replaceFailed }),
|
|
89
|
+
...(this.failed === undefined ? {} : { failed: this.failed }),
|
|
90
|
+
...(this.captureFailed === undefined ? {} : { captureFailed: this.captureFailed }),
|
|
91
|
+
...(this.search !== undefined && this.query?.regex && this.model.getLength() > LIVE_REGEX_LIMIT && !this.freshRegex ? { large: true } : {}),
|
|
92
|
+
...(this.query?.inSelection === true && this.bounds === null ? { noSelection: true } : {})
|
|
93
|
+
};
|
|
50
94
|
}
|
|
51
95
|
|
|
52
96
|
/* The range a search in the selection is held to. */
|
|
@@ -55,13 +99,78 @@ export class FindController {
|
|
|
55
99
|
}
|
|
56
100
|
|
|
57
101
|
get currentMark(): FindMark | null {
|
|
58
|
-
return this.index === null ? null : (this.marks[this.index] ?? null);
|
|
102
|
+
return this.outside ?? (this.index === null ? null : (this.marks[this.index] ?? null));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
matchesBetween(from: number, to: number): readonly FindMark[] {
|
|
106
|
+
let low = 0;
|
|
107
|
+
let high = this.marks.length;
|
|
108
|
+
while (low < high) {
|
|
109
|
+
const middle = (low + high) >>> 1;
|
|
110
|
+
if (this.marks[middle]!.to < from) {
|
|
111
|
+
low = middle + 1;
|
|
112
|
+
} else {
|
|
113
|
+
high = middle;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const first = low;
|
|
117
|
+
high = this.marks.length;
|
|
118
|
+
while (low < high) {
|
|
119
|
+
const middle = (low + high) >>> 1;
|
|
120
|
+
if (this.marks[middle]!.from <= to) {
|
|
121
|
+
low = middle + 1;
|
|
122
|
+
} else {
|
|
123
|
+
high = middle;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return this.marks.slice(first, low);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get currentMatch(): FindMatch | null {
|
|
130
|
+
if (this.search !== undefined && this.query?.regex) {
|
|
131
|
+
const mark = this.currentMark;
|
|
132
|
+
return this.freshRegex && this.retained?.from === mark?.from && this.retained?.to === mark?.to ? this.retained : null;
|
|
133
|
+
}
|
|
134
|
+
const matches = this.cachedMatches;
|
|
135
|
+
if (this.outside !== null && matches !== null) {
|
|
136
|
+
return {
|
|
137
|
+
...this.outside,
|
|
138
|
+
text: this.model.slice(this.outside.from, this.outside.to),
|
|
139
|
+
captures: [],
|
|
140
|
+
regex: false,
|
|
141
|
+
revision: this.model.getRevision()
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return this.index === null || matches === null ? null : (matches[this.index] ?? null);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get completeMatches(): readonly FindMatch[] | null {
|
|
148
|
+
return this.cache?.complete === true ? this.cachedMatches : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private get cachedMatches(): readonly FindMatch[] | null {
|
|
152
|
+
return this.query !== null && this.cache?.revision === this.model.getRevision() && this.cache.key === this.cacheKey() ? this.cache.matches : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private cacheKey(): string {
|
|
156
|
+
const query = this.query;
|
|
157
|
+
const scope = this.scope;
|
|
158
|
+
return JSON.stringify([
|
|
159
|
+
query?.text,
|
|
160
|
+
query?.caseSensitive === true,
|
|
161
|
+
query?.wholeWord === true,
|
|
162
|
+
query?.regex === true,
|
|
163
|
+
query?.inSelection === true,
|
|
164
|
+
scope?.from,
|
|
165
|
+
scope?.to
|
|
166
|
+
]);
|
|
59
167
|
}
|
|
60
168
|
|
|
61
169
|
/* A new query, or null to end the find. */
|
|
62
170
|
set(query: EditorFindQuery | null): void {
|
|
171
|
+
const oldKey = this.cacheKey();
|
|
63
172
|
const wasInSelection = this.query?.inSelection === true;
|
|
64
|
-
this.query = query === null || query.text === '' ? null : query;
|
|
173
|
+
this.query = query === null || query.text === '' ? null : { ...query };
|
|
65
174
|
if (this.query?.inSelection !== true) {
|
|
66
175
|
this.bounds = null;
|
|
67
176
|
} else if (!wasInSelection) {
|
|
@@ -71,11 +180,21 @@ export class FindController {
|
|
|
71
180
|
? null
|
|
72
181
|
: { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
|
|
73
182
|
}
|
|
183
|
+
if (this.cacheKey() !== oldKey) {
|
|
184
|
+
this.pendingAnchor = undefined;
|
|
185
|
+
this.pendingStep = undefined;
|
|
186
|
+
this.outside = null;
|
|
187
|
+
this.replaceFailed = undefined;
|
|
188
|
+
this.replaced = undefined;
|
|
189
|
+
}
|
|
74
190
|
this.refresh();
|
|
75
191
|
}
|
|
76
192
|
|
|
77
193
|
/* The text of a search in the selection moved with an edit. */
|
|
78
194
|
mapBounds(changes: readonly DocumentChange[]): void {
|
|
195
|
+
if (this.pendingAnchor !== undefined) {
|
|
196
|
+
this.pendingAnchor = mapOffset(this.pendingAnchor, changes);
|
|
197
|
+
}
|
|
79
198
|
if (this.bounds !== null) {
|
|
80
199
|
this.bounds = { from: mapOffset(this.bounds.from, changes), to: mapOffset(this.bounds.to, changes) };
|
|
81
200
|
}
|
|
@@ -86,43 +205,525 @@ export class FindController {
|
|
|
86
205
|
* previous one was, or from the cursor, so each letter typed keeps the place instead of starting over.
|
|
87
206
|
*/
|
|
88
207
|
refresh(): void {
|
|
208
|
+
if (this.revision !== this.model.getRevision()) {
|
|
209
|
+
this.revision = this.model.getRevision();
|
|
210
|
+
this.replaced = undefined;
|
|
211
|
+
}
|
|
89
212
|
const previous = this.currentMark;
|
|
90
213
|
const query = this.query;
|
|
214
|
+
if (this.search !== undefined && query?.regex) {
|
|
215
|
+
this.refreshRegex(previous);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.resetRegex(query === null || this.search?.busy === true);
|
|
219
|
+
|
|
91
220
|
if (query === null) {
|
|
92
221
|
this.marks = [];
|
|
222
|
+
this.cache = undefined;
|
|
93
223
|
this.index = null;
|
|
94
224
|
return;
|
|
95
225
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
226
|
+
const revision = this.model.getRevision();
|
|
227
|
+
const key = this.cacheKey();
|
|
228
|
+
if (this.cache?.revision !== revision || this.cache.key !== key) {
|
|
229
|
+
let found: FindMatch[] = [];
|
|
230
|
+
try {
|
|
231
|
+
const inSelection = query.inSelection === true;
|
|
232
|
+
if (!inSelection || this.bounds !== null) {
|
|
233
|
+
found = this.model.find(query.text, {
|
|
102
234
|
caseSensitive: query.caseSensitive,
|
|
103
235
|
wholeWord: query.wholeWord,
|
|
104
236
|
regex: query.regex,
|
|
105
|
-
maxResults: FIND_LIMIT,
|
|
237
|
+
maxResults: FIND_LIMIT + 1,
|
|
106
238
|
...(inSelection ? this.bounds : {})
|
|
107
|
-
})
|
|
108
|
-
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// A pattern that does not parse finds nothing; the find bar says it is invalid.
|
|
109
243
|
}
|
|
110
|
-
|
|
111
|
-
// A pattern that does not parse finds nothing; the find bar says it is invalid.
|
|
244
|
+
this.cache = { revision, key, matches: found.slice(0, FIND_LIMIT), complete: found.length <= FIND_LIMIT };
|
|
112
245
|
}
|
|
246
|
+
const found = this.cache.matches;
|
|
113
247
|
this.marks = found;
|
|
114
248
|
const selection = this.model.getPrimary();
|
|
115
249
|
const anchor = this.pendingAnchor ?? previous?.from ?? Math.min(selection.anchor, selection.head);
|
|
116
250
|
this.pendingAnchor = undefined;
|
|
117
|
-
|
|
118
|
-
this.index = found.length === 0 ? null : Math.max(0, next);
|
|
251
|
+
this.chooseCurrent(anchor, 1, !this.cache.complete);
|
|
119
252
|
}
|
|
120
253
|
|
|
121
254
|
step(direction: 1 | -1): void {
|
|
255
|
+
this.replaced = undefined;
|
|
256
|
+
if (this.seeking) {
|
|
257
|
+
this.pendingStep = direction;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (this.search !== undefined && this.query?.regex && !this.freshRegex) {
|
|
261
|
+
this.pendingStep = direction;
|
|
262
|
+
if (!this.searching) {
|
|
263
|
+
this.startRegex(true);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
122
267
|
const count = this.marks.length;
|
|
123
268
|
if (count === 0) {
|
|
124
269
|
return;
|
|
125
270
|
}
|
|
271
|
+
const incomplete = this.regexCache?.complete === false || this.cache?.complete === false;
|
|
272
|
+
const current = this.currentMark;
|
|
273
|
+
if (
|
|
274
|
+
this.search !== undefined &&
|
|
275
|
+
incomplete &&
|
|
276
|
+
current !== null &&
|
|
277
|
+
(this.outside !== null || (direction === 1 ? this.index === count - 1 : this.index === 0))
|
|
278
|
+
) {
|
|
279
|
+
let offset = direction === -1 ? current.from : current.to;
|
|
280
|
+
if (direction === 1 && current.from === current.to) {
|
|
281
|
+
offset += (this.model.getText().codePointAt(offset) ?? 0) > 0xffff ? 2 : 1;
|
|
282
|
+
}
|
|
283
|
+
if (offset > this.model.getLength()) {
|
|
284
|
+
offset = 0;
|
|
285
|
+
}
|
|
286
|
+
this.seek(offset, direction);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.outside = null;
|
|
126
290
|
this.index = this.index === null ? (direction === 1 ? 0 : count - 1) : (this.index + direction + count) % count;
|
|
291
|
+
this.captureFailed = undefined;
|
|
292
|
+
this.preparePreview();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private get freshRegex(): boolean {
|
|
296
|
+
return (
|
|
297
|
+
this.failed === undefined && !this.searching && this.regexCache?.revision === this.model.getRevision() && this.regexCache.key === this.cacheKey()
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private resetRegex(release: boolean): void {
|
|
302
|
+
this.epoch++;
|
|
303
|
+
this.cancelSearch?.();
|
|
304
|
+
this.cancelSearch = undefined;
|
|
305
|
+
this.cancelMatch?.();
|
|
306
|
+
this.cancelMatch = undefined;
|
|
307
|
+
this.regexCache = undefined;
|
|
308
|
+
this.outside = null;
|
|
309
|
+
this.seeking = false;
|
|
310
|
+
this.snapshotMemo = undefined;
|
|
311
|
+
this.retained = null;
|
|
312
|
+
this.searching = false;
|
|
313
|
+
this.failed = undefined;
|
|
314
|
+
this.captureFailed = undefined;
|
|
315
|
+
if (release) {
|
|
316
|
+
this.search?.release();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private refreshRegex(previous: FindMark | null): void {
|
|
321
|
+
if (this.freshRegex) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const failure = this.failed;
|
|
325
|
+
const retryBlocked = failure === 'timeout' || failure === 'unavailable';
|
|
326
|
+
const sameQuery = this.regexCache?.key === this.cacheKey();
|
|
327
|
+
this.resetRegex(false);
|
|
328
|
+
this.cache = undefined;
|
|
329
|
+
this.marks = [];
|
|
330
|
+
this.index = null;
|
|
331
|
+
if (retryBlocked && sameQuery) {
|
|
332
|
+
this.failed = failure;
|
|
333
|
+
this.regexCache = { revision: this.model.getRevision(), key: this.cacheKey(), complete: false };
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.pendingAnchor ??= previous?.from;
|
|
337
|
+
this.startRegex(false);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private startRegex(explicit: boolean): void {
|
|
341
|
+
const query = this.query;
|
|
342
|
+
if (query === null || this.search === undefined || this.disposed) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (query.inSelection && this.bounds === null) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!explicit && this.model.getLength() > LIVE_REGEX_LIMIT) {
|
|
349
|
+
this.pendingStep = undefined;
|
|
350
|
+
this.pendingAnchor = undefined;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const epoch = this.epoch;
|
|
354
|
+
const key = this.cacheKey();
|
|
355
|
+
const revision = this.model.getRevision();
|
|
356
|
+
this.failed = undefined;
|
|
357
|
+
this.searching = true;
|
|
358
|
+
this.cancelSearch?.();
|
|
359
|
+
this.cancelSearch = (this.options?.schedule ?? scheduleFind)(
|
|
360
|
+
() => {
|
|
361
|
+
this.cancelSearch = undefined;
|
|
362
|
+
const snapshot = this.snapshot();
|
|
363
|
+
void this.search!.request(
|
|
364
|
+
(client) => {
|
|
365
|
+
if (epoch !== this.epoch || this.disposed) {
|
|
366
|
+
throw new EditorSearchError('superseded', 'The find query changed.');
|
|
367
|
+
}
|
|
368
|
+
return client.search(snapshot, query, { ...this.scope, maxResults: FIND_LIMIT, ...(explicit ? { deadline: 10000 } : {}) });
|
|
369
|
+
},
|
|
370
|
+
explicit ? 'command' : 'live',
|
|
371
|
+
() => epoch === this.epoch && revision === this.model.getRevision() && !this.disposed
|
|
372
|
+
)
|
|
373
|
+
.then((result) => {
|
|
374
|
+
if (epoch !== this.epoch || revision !== this.model.getRevision() || this.disposed) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
this.regexCache = { revision, key, complete: !result.limited };
|
|
378
|
+
const marks: FindMark[] = [];
|
|
379
|
+
for (let index = 0; index < result.ranges.length; index += 2) {
|
|
380
|
+
marks.push({ from: result.ranges[index]!, to: result.ranges[index + 1]! });
|
|
381
|
+
}
|
|
382
|
+
this.marks = marks;
|
|
383
|
+
const selection = this.model.getPrimary();
|
|
384
|
+
const anchor = this.pendingAnchor ?? Math.min(selection.anchor, selection.head);
|
|
385
|
+
this.pendingAnchor = undefined;
|
|
386
|
+
const direction = this.pendingStep;
|
|
387
|
+
this.pendingStep = undefined;
|
|
388
|
+
this.searching = false;
|
|
389
|
+
this.chooseCurrent(anchor, direction ?? 1, result.limited);
|
|
390
|
+
this.options?.changed(true);
|
|
391
|
+
this.preparePreview();
|
|
392
|
+
})
|
|
393
|
+
.catch((error) => {
|
|
394
|
+
if (epoch !== this.epoch || this.disposed) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
this.searching = false;
|
|
398
|
+
if (error instanceof EditorSearchError && error.kind === 'superseded') {
|
|
399
|
+
this.options?.changed();
|
|
400
|
+
this.startRegex(this.pendingStep !== undefined);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.pendingAnchor = undefined;
|
|
404
|
+
this.pendingStep = undefined;
|
|
405
|
+
this.failed =
|
|
406
|
+
error instanceof EditorSearchError
|
|
407
|
+
? error.kind === 'syntax'
|
|
408
|
+
? 'invalid'
|
|
409
|
+
: error.kind === 'timeout' || error.kind === 'unavailable'
|
|
410
|
+
? error.kind
|
|
411
|
+
: 'error'
|
|
412
|
+
: 'error';
|
|
413
|
+
this.regexCache = { revision, key, complete: false };
|
|
414
|
+
this.options?.changed();
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
explicit || this.model.getLength() <= 1_000_000 ? 0 : 150
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private snapshot(): EditorSearchSnapshot {
|
|
422
|
+
const revision = this.model.getRevision();
|
|
423
|
+
if (this.snapshotMemo?.revision !== revision) {
|
|
424
|
+
this.snapshotMemo = { revision, text: this.model.getText() };
|
|
425
|
+
}
|
|
426
|
+
return this.snapshotMemo;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
setPreview(active: boolean): void {
|
|
430
|
+
this.preview = active;
|
|
431
|
+
this.preparePreview();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private preparePreview(): void {
|
|
435
|
+
this.cancelMatch?.();
|
|
436
|
+
this.cancelMatch = undefined;
|
|
437
|
+
if (
|
|
438
|
+
this.preview &&
|
|
439
|
+
this.search !== undefined &&
|
|
440
|
+
this.query?.regex &&
|
|
441
|
+
this.freshRegex &&
|
|
442
|
+
this.currentMatch === null &&
|
|
443
|
+
this.captureFailed === undefined
|
|
444
|
+
) {
|
|
445
|
+
this.cancelMatch = (this.options?.schedule ?? scheduleFind)(() => {
|
|
446
|
+
this.cancelMatch = undefined;
|
|
447
|
+
void this.ensureMatch().catch(() => {});
|
|
448
|
+
}, 50);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async ensureMatch(command = false): Promise<FindMatch | null> {
|
|
453
|
+
if (this.currentMatch !== null || this.search === undefined || !this.query?.regex || !this.freshRegex) {
|
|
454
|
+
return this.currentMatch;
|
|
455
|
+
}
|
|
456
|
+
const mark = this.currentMark;
|
|
457
|
+
if (mark === null) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const query = this.query;
|
|
461
|
+
const epoch = this.epoch;
|
|
462
|
+
const snapshot = this.snapshot();
|
|
463
|
+
if (command) {
|
|
464
|
+
this.captureFailed = undefined;
|
|
465
|
+
}
|
|
466
|
+
const current = (): boolean =>
|
|
467
|
+
!this.disposed &&
|
|
468
|
+
epoch === this.epoch &&
|
|
469
|
+
this.model.getRevision() === snapshot.revision &&
|
|
470
|
+
this.currentMark?.from === mark.from &&
|
|
471
|
+
this.currentMark?.to === mark.to;
|
|
472
|
+
try {
|
|
473
|
+
const found = await this.search.request(
|
|
474
|
+
(client) => {
|
|
475
|
+
if (!current()) {
|
|
476
|
+
throw new EditorSearchError('superseded', 'The current find match changed.');
|
|
477
|
+
}
|
|
478
|
+
return client.match(snapshot, query, { ...mark, deadline: 10000 });
|
|
479
|
+
},
|
|
480
|
+
command ? 'command' : 'preview'
|
|
481
|
+
);
|
|
482
|
+
if (!current()) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
this.retained = found;
|
|
486
|
+
this.captureFailed = undefined;
|
|
487
|
+
this.options?.changed();
|
|
488
|
+
return found;
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (current() && !(error instanceof EditorSearchError && error.kind === 'superseded')) {
|
|
491
|
+
this.captureFailed = error instanceof EditorSearchError && (error.kind === 'timeout' || error.kind === 'unavailable') ? error.kind : 'error';
|
|
492
|
+
this.options?.changed();
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
127
496
|
}
|
|
497
|
+
|
|
498
|
+
private chooseCurrent(anchor: number, direction: 1 | -1, limited: boolean): void {
|
|
499
|
+
if (this.marks.length === 0) {
|
|
500
|
+
this.index = null;
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const next = direction === 1 ? this.marks.findIndex((mark) => mark.from >= anchor) : this.marks.findLastIndex((mark) => mark.from < anchor);
|
|
504
|
+
if (limited && this.search !== undefined && (next === -1 || (direction === -1 && next === this.marks.length - 1))) {
|
|
505
|
+
this.index = null;
|
|
506
|
+
this.seek(anchor, direction);
|
|
507
|
+
} else {
|
|
508
|
+
this.index = next === -1 ? (direction === 1 ? 0 : this.marks.length - 1) : next;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private seek(offset: number, direction: 1 | -1): void {
|
|
513
|
+
const query = this.query;
|
|
514
|
+
if (this.search === undefined || query === null) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const epoch = this.epoch;
|
|
518
|
+
const snapshot = this.snapshot();
|
|
519
|
+
const scope = this.scope;
|
|
520
|
+
const valid = (): boolean => !this.disposed && epoch === this.epoch && snapshot.revision === this.model.getRevision();
|
|
521
|
+
this.seeking = true;
|
|
522
|
+
this.failed = undefined;
|
|
523
|
+
this.retained = null;
|
|
524
|
+
this.captureFailed = undefined;
|
|
525
|
+
void this.search
|
|
526
|
+
.request((client) => client.next(snapshot, query, offset, { ...scope, backwards: direction === -1, wrap: true, deadline: 10000 }), 'command', valid)
|
|
527
|
+
.then((range) => {
|
|
528
|
+
if (!valid()) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
this.seeking = false;
|
|
532
|
+
this.outside = null;
|
|
533
|
+
this.index = range === null ? null : this.marks.findIndex((mark) => mark.from === range.from && mark.to === range.to);
|
|
534
|
+
if (this.index === -1) {
|
|
535
|
+
this.index = null;
|
|
536
|
+
this.outside = range;
|
|
537
|
+
}
|
|
538
|
+
this.options?.changed(true);
|
|
539
|
+
this.preparePreview();
|
|
540
|
+
const pending = this.pendingStep;
|
|
541
|
+
this.pendingStep = undefined;
|
|
542
|
+
if (pending !== undefined) {
|
|
543
|
+
this.step(pending);
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
.catch((error) => {
|
|
547
|
+
if (valid()) {
|
|
548
|
+
this.seeking = false;
|
|
549
|
+
this.failed = searchFailure(error);
|
|
550
|
+
this.options?.changed();
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async nextFromCursor(query: EditorFindQuery, offset: number, direction: 1 | -1): Promise<FindMark | null> {
|
|
556
|
+
if (this.search === undefined) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const snapshot = this.snapshot();
|
|
560
|
+
const epoch = this.epoch;
|
|
561
|
+
const valid = (): boolean => !this.disposed && epoch === this.epoch && snapshot.revision === this.model.getRevision();
|
|
562
|
+
this.cursorRequests++;
|
|
563
|
+
try {
|
|
564
|
+
const range = await this.search.request(
|
|
565
|
+
(client) => client.next(snapshot, query, offset, { backwards: direction === -1, wrap: true, deadline: 10000 }),
|
|
566
|
+
'command',
|
|
567
|
+
valid
|
|
568
|
+
);
|
|
569
|
+
return valid() ? range : null;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (valid()) {
|
|
572
|
+
this.failed = searchFailure(error);
|
|
573
|
+
this.options?.changed();
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
} finally {
|
|
577
|
+
this.cursorRequests--;
|
|
578
|
+
if (this.query === null && this.cursorRequests === 0) {
|
|
579
|
+
this.search.release();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
reportReplaceFailed(failure: NonNullable<EditorFindState['replaceFailed']>): void {
|
|
585
|
+
this.replaceFailed = failure;
|
|
586
|
+
this.replaced = undefined;
|
|
587
|
+
this.options?.changed();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
reportReplaced(count: number): void {
|
|
591
|
+
this.replaced = count;
|
|
592
|
+
this.replaceFailed = undefined;
|
|
593
|
+
this.options?.changed();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async replaceOne(replacement: string, preserveCase: boolean, canApply: () => boolean): Promise<boolean> {
|
|
597
|
+
const query = this.query,
|
|
598
|
+
mark = this.currentMark;
|
|
599
|
+
if (query === null || mark === null || this.search === undefined || !canApply()) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
const snapshot = this.snapshot();
|
|
603
|
+
const epoch = this.epoch;
|
|
604
|
+
const valid = (): boolean =>
|
|
605
|
+
!this.disposed &&
|
|
606
|
+
epoch === this.epoch &&
|
|
607
|
+
snapshot.revision === this.model.getRevision() &&
|
|
608
|
+
canApply() &&
|
|
609
|
+
this.currentMark?.from === mark.from &&
|
|
610
|
+
this.currentMark?.to === mark.to;
|
|
611
|
+
this.replacing++;
|
|
612
|
+
this.replaceEpoch = epoch;
|
|
613
|
+
this.replaceFailed = undefined;
|
|
614
|
+
this.options?.changed();
|
|
615
|
+
try {
|
|
616
|
+
const page = await this.search.request(
|
|
617
|
+
(client) =>
|
|
618
|
+
client.replacements(snapshot, query, replacement, {
|
|
619
|
+
from: mark.from,
|
|
620
|
+
to: mark.to,
|
|
621
|
+
preserveCase,
|
|
622
|
+
maxEdits: 1,
|
|
623
|
+
maxCharacters: MAX_REPLACEMENT_CHARACTERS,
|
|
624
|
+
deadline: 10000
|
|
625
|
+
}),
|
|
626
|
+
'command',
|
|
627
|
+
valid
|
|
628
|
+
);
|
|
629
|
+
if (!valid() || page.ranges.length !== 3 || page.ranges[0] !== mark.from || page.ranges[1] !== mark.to || page.next !== null) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
this.pendingAnchor = mark.to;
|
|
633
|
+
const changed = this.model.applyEdits([{ from: mark.from, to: mark.to, text: page.text }], {
|
|
634
|
+
source: 'command',
|
|
635
|
+
expectedRevision: snapshot.revision
|
|
636
|
+
});
|
|
637
|
+
if (!changed) {
|
|
638
|
+
this.pendingAnchor = undefined;
|
|
639
|
+
}
|
|
640
|
+
return changed;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (valid()) {
|
|
643
|
+
const failure = searchFailure(error);
|
|
644
|
+
this.reportReplaceFailed(error instanceof EditorSearchError && error.kind === 'range' ? 'limit' : failure === 'invalid' ? 'error' : failure);
|
|
645
|
+
}
|
|
646
|
+
return false;
|
|
647
|
+
} finally {
|
|
648
|
+
this.replacing--;
|
|
649
|
+
if (!this.disposed) {
|
|
650
|
+
this.options?.changed();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async replaceAll(replacement: string, preserveCase: boolean, canApply: () => boolean): Promise<number> {
|
|
656
|
+
const query = this.query;
|
|
657
|
+
if (query === null || this.search === undefined || this.state.noSelection || !canApply()) {
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
const snapshot = this.snapshot();
|
|
661
|
+
const epoch = this.epoch;
|
|
662
|
+
const scope = this.scope;
|
|
663
|
+
const valid = (): boolean => !this.disposed && epoch === this.epoch && snapshot.revision === this.model.getRevision() && canApply();
|
|
664
|
+
this.replacing++;
|
|
665
|
+
this.replaceEpoch = epoch;
|
|
666
|
+
this.replaceFailed = undefined;
|
|
667
|
+
this.replaced = undefined;
|
|
668
|
+
this.options?.changed();
|
|
669
|
+
try {
|
|
670
|
+
const page = await this.search.request(
|
|
671
|
+
(client) =>
|
|
672
|
+
client.replacements(snapshot, query, replacement, {
|
|
673
|
+
...scope,
|
|
674
|
+
preserveCase,
|
|
675
|
+
maxEdits: MAX_REPLACEMENT_PAGE,
|
|
676
|
+
maxCharacters: MAX_REPLACEMENT_CHARACTERS,
|
|
677
|
+
deadline: 10000
|
|
678
|
+
}),
|
|
679
|
+
'command',
|
|
680
|
+
valid
|
|
681
|
+
);
|
|
682
|
+
if (!valid()) {
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
685
|
+
if (page.next !== null) {
|
|
686
|
+
this.replaceFailed = 'limit';
|
|
687
|
+
return 0;
|
|
688
|
+
}
|
|
689
|
+
const edits: TextEdit[] = [];
|
|
690
|
+
let position = 0;
|
|
691
|
+
for (let index = 0; index < page.ranges.length; index += 3) {
|
|
692
|
+
const length = page.ranges[index + 2]!;
|
|
693
|
+
edits.push({ from: page.ranges[index]!, to: page.ranges[index + 1]!, text: page.text.slice(position, position + length) });
|
|
694
|
+
position += length;
|
|
695
|
+
}
|
|
696
|
+
const changed = this.model.applyEdits(edits, { source: 'command', expectedRevision: snapshot.revision });
|
|
697
|
+
const count = changed ? edits.length : 0;
|
|
698
|
+
this.reportReplaced(count);
|
|
699
|
+
return count;
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if (!this.disposed && epoch === this.epoch) {
|
|
702
|
+
const failure = searchFailure(error);
|
|
703
|
+
this.replaceFailed = error instanceof EditorSearchError && error.kind === 'range' ? 'limit' : failure === 'invalid' ? 'error' : failure;
|
|
704
|
+
}
|
|
705
|
+
return 0;
|
|
706
|
+
} finally {
|
|
707
|
+
this.replacing--;
|
|
708
|
+
if (!this.disposed) {
|
|
709
|
+
this.options?.changed();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
dispose(): void {
|
|
715
|
+
this.disposed = true;
|
|
716
|
+
this.resetRegex(false);
|
|
717
|
+
this.search?.dispose();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function searchFailure(error: unknown): NonNullable<EditorFindState['failed']> {
|
|
722
|
+
return error instanceof EditorSearchError
|
|
723
|
+
? error.kind === 'syntax'
|
|
724
|
+
? 'invalid'
|
|
725
|
+
: error.kind === 'timeout' || error.kind === 'unavailable'
|
|
726
|
+
? error.kind
|
|
727
|
+
: 'error'
|
|
728
|
+
: 'error';
|
|
128
729
|
}
|