@adecore/editor 0.22.1 → 0.23.1

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.
Files changed (94) hide show
  1. package/dist/attribution.d.ts +9 -8
  2. package/dist/attribution.js +44 -9
  3. package/dist/bidi.d.ts +44 -0
  4. package/dist/bidi.js +26 -0
  5. package/dist/column-selection.js +2 -1
  6. package/dist/controller.d.ts +6 -1
  7. package/dist/controller.js +93 -3
  8. package/dist/editor.css +81 -1
  9. package/dist/engine.js +189 -41
  10. package/dist/fake.d.ts +45 -5
  11. package/dist/fake.js +121 -13
  12. package/dist/find-search.d.ts +26 -0
  13. package/dist/find-search.js +130 -0
  14. package/dist/find.d.ts +50 -2
  15. package/dist/find.js +575 -25
  16. package/dist/geometry-stops.d.ts +6 -0
  17. package/dist/geometry-stops.js +47 -0
  18. package/dist/grammar-preparation.d.ts +29 -0
  19. package/dist/grammar-preparation.js +147 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +2 -0
  22. package/dist/keymap-table.d.ts +10 -1
  23. package/dist/keymap-table.js +37 -1
  24. package/dist/keymap.d.ts +1 -0
  25. package/dist/keymap.js +4 -1
  26. package/dist/layout-changes.d.ts +6 -0
  27. package/dist/layout-changes.js +27 -0
  28. package/dist/layout.d.ts +45 -2
  29. package/dist/layout.js +202 -58
  30. package/dist/offsets.d.ts +1 -0
  31. package/dist/offsets.js +23 -0
  32. package/dist/outline.js +5 -4
  33. package/dist/paint.d.ts +11 -3
  34. package/dist/paint.js +180 -34
  35. package/dist/search-client.d.ts +27 -0
  36. package/dist/search-client.js +210 -0
  37. package/dist/search-protocol.d.ts +103 -0
  38. package/dist/search-protocol.js +3 -0
  39. package/dist/search-worker.d.ts +11 -0
  40. package/dist/search-worker.js +137 -0
  41. package/dist/semantic.d.ts +2 -1
  42. package/dist/semantic.js +6 -1
  43. package/dist/shiki-document.d.ts +8 -0
  44. package/dist/shiki-document.js +93 -0
  45. package/dist/shiki-worker.d.ts +65 -0
  46. package/dist/shiki-worker.js +265 -0
  47. package/dist/shiki.d.ts +1 -0
  48. package/dist/shiki.js +96 -33
  49. package/dist/testing.d.ts +1 -0
  50. package/dist/tokens.d.ts +2 -0
  51. package/dist/tokens.js +37 -7
  52. package/dist/touch.d.ts +27 -0
  53. package/dist/touch.js +100 -0
  54. package/dist/types.d.ts +86 -2
  55. package/dist/uniform-geometry.d.ts +7 -0
  56. package/dist/uniform-geometry.js +49 -0
  57. package/dist/view.d.ts +69 -17
  58. package/dist/view.js +427 -97
  59. package/dist/worker-client.d.ts +37 -0
  60. package/dist/worker-client.js +195 -0
  61. package/package.json +17 -2
  62. package/src/attribution.ts +50 -14
  63. package/src/bidi.ts +32 -0
  64. package/src/column-selection.ts +2 -1
  65. package/src/controller.ts +97 -3
  66. package/src/editor.css +81 -1
  67. package/src/engine.ts +222 -42
  68. package/src/fake.ts +157 -14
  69. package/src/find-search.ts +165 -0
  70. package/src/find.ts +622 -21
  71. package/src/geometry-stops.ts +51 -0
  72. package/src/grammar-preparation.ts +158 -0
  73. package/src/index.ts +10 -0
  74. package/src/keymap-table.ts +44 -2
  75. package/src/keymap.ts +5 -1
  76. package/src/layout-changes.ts +33 -0
  77. package/src/layout.ts +262 -63
  78. package/src/offsets.ts +24 -0
  79. package/src/outline.ts +5 -4
  80. package/src/paint.ts +202 -36
  81. package/src/search-client.ts +306 -0
  82. package/src/search-protocol.ts +100 -0
  83. package/src/search-worker.ts +171 -0
  84. package/src/semantic.ts +8 -2
  85. package/src/shiki-document.ts +143 -0
  86. package/src/shiki-worker.ts +344 -0
  87. package/src/shiki.ts +103 -33
  88. package/src/testing.ts +2 -0
  89. package/src/tokens.ts +36 -7
  90. package/src/touch.ts +134 -0
  91. package/src/types.ts +178 -8
  92. package/src/uniform-geometry.ts +57 -0
  93. package/src/view.ts +505 -102
  94. 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
- /* Past this a count means nothing to a reader, and marking every match would stall the editor. */
11
- export const FIND_LIMIT = 10000;
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 { count: this.marks.length, current: this.index, ...(this.query?.inSelection === true && this.bounds === null ? { noSelection: true } : {}) };
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
- let found: FindMark[] = [];
97
- try {
98
- const inSelection = query.inSelection === true;
99
- if (!inSelection || this.bounds !== null) {
100
- found = this.model
101
- .find(query.text, {
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
- .map((match) => ({ from: match.from, to: match.to }));
239
+ });
240
+ }
241
+ } catch {
242
+ // A pattern that does not parse finds nothing; the find bar says it is invalid.
109
243
  }
110
- } catch {
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
- const next = found.findIndex((mark) => mark.from >= anchor);
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
  }