@adecore/editor 0.22.1 → 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/fake.ts
CHANGED
|
@@ -5,7 +5,9 @@ import type {
|
|
|
5
5
|
EditorBlock,
|
|
6
6
|
EditorCodeBlockOptions,
|
|
7
7
|
EditorLineAction,
|
|
8
|
+
EditorReplacement,
|
|
8
9
|
EditorLineHighlight,
|
|
10
|
+
LineToken,
|
|
9
11
|
EditorChangeMark,
|
|
10
12
|
EditorCodeVision,
|
|
11
13
|
EditorClick,
|
|
@@ -22,9 +24,14 @@ import type {
|
|
|
22
24
|
EditorHover,
|
|
23
25
|
EditorIndentation,
|
|
24
26
|
EditorGhostText,
|
|
27
|
+
EditorInlayClick,
|
|
28
|
+
EditorInlayClickHandler,
|
|
25
29
|
EditorInlayHint,
|
|
30
|
+
EditorInlayTarget,
|
|
26
31
|
EditorKeyHandler,
|
|
32
|
+
EditorInputPopup,
|
|
27
33
|
EditorMarker,
|
|
34
|
+
EditorMessages,
|
|
28
35
|
EditorOptions,
|
|
29
36
|
EditorPosition,
|
|
30
37
|
EditorRemoteCursor,
|
|
@@ -38,6 +45,7 @@ import type {
|
|
|
38
45
|
EditorFolds,
|
|
39
46
|
EditorSemanticToken,
|
|
40
47
|
EditorSmartKeys,
|
|
48
|
+
EditorApplyEditsOptions,
|
|
41
49
|
EditorTextChange,
|
|
42
50
|
EditorTrackedRange,
|
|
43
51
|
EditorTheme,
|
|
@@ -59,7 +67,7 @@ export class FakeEditor implements Editor {
|
|
|
59
67
|
readonly language: string | undefined;
|
|
60
68
|
readonly path: string | undefined;
|
|
61
69
|
readonly column: number | null;
|
|
62
|
-
|
|
70
|
+
scrollTop: number | null;
|
|
63
71
|
wrap: boolean;
|
|
64
72
|
theme: EditorTheme;
|
|
65
73
|
readOnly: boolean;
|
|
@@ -130,7 +138,7 @@ export class FakeEditor implements Editor {
|
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
/* The text becomes `text`, and listeners of the text hear the one stretch that differs, as the real editor reports it. */
|
|
133
|
-
private replaceText(text: string, source: EditorTextChange['source'] = 'external'): void {
|
|
141
|
+
private replaceText(text: string, source: EditorTextChange['source'] = 'external', selections?: readonly EditorRange[]): void {
|
|
134
142
|
const span = changedSpan(this.text, text);
|
|
135
143
|
const before = this.text;
|
|
136
144
|
this.text = text;
|
|
@@ -139,7 +147,11 @@ export class FakeEditor implements Editor {
|
|
|
139
147
|
}
|
|
140
148
|
// As in the real editor the caret lands behind what was typed, and listeners of the caret hear it before those of the text.
|
|
141
149
|
if (source !== 'external') {
|
|
142
|
-
|
|
150
|
+
if (selections === undefined) {
|
|
151
|
+
this.moveCaret(positionIn(text, span.start + span.text.length));
|
|
152
|
+
} else {
|
|
153
|
+
this.setSelections(selections);
|
|
154
|
+
}
|
|
143
155
|
}
|
|
144
156
|
this.followTracked(span.start, span.end, span.text.length);
|
|
145
157
|
this.ghost = null;
|
|
@@ -250,6 +262,10 @@ export class FakeEditor implements Editor {
|
|
|
250
262
|
return this.readOnly ? 0 : this.findState.count;
|
|
251
263
|
}
|
|
252
264
|
|
|
265
|
+
async replaceAllAsync(replacement: string, options: EditorReplaceOptions = {}): Promise<number> {
|
|
266
|
+
return this.replaceAll(replacement, options);
|
|
267
|
+
}
|
|
268
|
+
|
|
253
269
|
setReplacePreview(replacement: string | null): void {
|
|
254
270
|
this.replacePreview = replacement;
|
|
255
271
|
}
|
|
@@ -288,11 +304,19 @@ export class FakeEditor implements Editor {
|
|
|
288
304
|
blocks: readonly EditorBlock[] | null = null;
|
|
289
305
|
private readonly scopes = new Set<(scope: readonly EditorBlock[]) => void>();
|
|
290
306
|
|
|
291
|
-
/*
|
|
292
|
-
changeMarks: readonly EditorChangeMark[]
|
|
307
|
+
/* The marks of every layer, in the order the layers last became non-empty. */
|
|
308
|
+
get changeMarks(): readonly EditorChangeMark[] {
|
|
309
|
+
return [...this.changeLayers.values()].flat();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private readonly changeLayers = new Map<string, readonly EditorChangeMark[]>();
|
|
293
313
|
|
|
294
|
-
setChangeMarks(marks: readonly EditorChangeMark[]): void {
|
|
295
|
-
|
|
314
|
+
setChangeMarks(marks: readonly EditorChangeMark[], layer = ''): void {
|
|
315
|
+
if (marks.length === 0) {
|
|
316
|
+
this.changeLayers.delete(layer);
|
|
317
|
+
} else {
|
|
318
|
+
this.changeLayers.set(layer, marks);
|
|
319
|
+
}
|
|
296
320
|
}
|
|
297
321
|
|
|
298
322
|
/* The tinted lines the host set last. */
|
|
@@ -305,6 +329,43 @@ export class FakeEditor implements Editor {
|
|
|
305
329
|
/* What the host drew as code outside the document: the text and how, and into which element. */
|
|
306
330
|
codeBlocks: { container: HTMLElement; text: string; options: EditorCodeBlockOptions }[] = [];
|
|
307
331
|
|
|
332
|
+
lineNumbers: ((line: number) => string | null) | null = null;
|
|
333
|
+
lineTokens: ((line: number) => readonly LineToken[] | null | undefined) | null = null;
|
|
334
|
+
|
|
335
|
+
setLineNumbers(labels: ((line: number) => string | null) | null): void {
|
|
336
|
+
this.lineNumbers = labels;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
lineLabels: ((line: number) => string | null) | null = null;
|
|
340
|
+
|
|
341
|
+
setLineLabels(labels: ((line: number) => string | null) | null): void {
|
|
342
|
+
this.lineLabels = labels;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* What the input points at, as the last call left it. */
|
|
346
|
+
inputPopup: EditorInputPopup | null = null;
|
|
347
|
+
|
|
348
|
+
setInputPopup(popup: EditorInputPopup | null): void {
|
|
349
|
+
this.inputPopup = popup;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* Every sentence said, oldest first. */
|
|
353
|
+
announcements: string[] = [];
|
|
354
|
+
|
|
355
|
+
announce(message: string): void {
|
|
356
|
+
this.announcements.push(message);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
messages: Partial<EditorMessages> = {};
|
|
360
|
+
|
|
361
|
+
setMessages(messages: Partial<EditorMessages>): void {
|
|
362
|
+
this.messages = { ...this.messages, ...messages };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
setLineTokens(tokens: ((line: number) => readonly LineToken[] | null | undefined) | null): void {
|
|
366
|
+
this.lineTokens = tokens;
|
|
367
|
+
}
|
|
368
|
+
|
|
308
369
|
renderCode(container: HTMLElement, text: string, options: EditorCodeBlockOptions = {}): void {
|
|
309
370
|
this.codeBlocks.push({ container, text, options });
|
|
310
371
|
}
|
|
@@ -359,12 +420,22 @@ export class FakeEditor implements Editor {
|
|
|
359
420
|
const before = this.drawnWidgets.get(owner) ?? new Set<string>();
|
|
360
421
|
for (const widget of widgets) {
|
|
361
422
|
if (!before.has(widget.id)) {
|
|
362
|
-
widget.render({} as HTMLElement);
|
|
423
|
+
widget.render(this.element.ownerDocument?.createElement('div') ?? ({} as HTMLElement));
|
|
363
424
|
}
|
|
364
425
|
}
|
|
365
426
|
this.drawnWidgets.set(owner, new Set(widgets.map((widget) => widget.id)));
|
|
366
427
|
}
|
|
367
428
|
|
|
429
|
+
readonly replacementsByOwner = new Map<string, readonly EditorReplacement[]>();
|
|
430
|
+
|
|
431
|
+
setReplacements(replacements: readonly EditorReplacement[], owner = 'default'): void {
|
|
432
|
+
if (replacements.length === 0) {
|
|
433
|
+
this.replacementsByOwner.delete(owner);
|
|
434
|
+
} else {
|
|
435
|
+
this.replacementsByOwner.set(owner, replacements);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
368
439
|
readonly lineActionsByOwner = new Map<string, readonly EditorLineAction[]>();
|
|
369
440
|
|
|
370
441
|
setLineActions(actions: readonly EditorLineAction[], owner = 'default'): void {
|
|
@@ -487,6 +558,7 @@ export class FakeEditor implements Editor {
|
|
|
487
558
|
charSize = { width: 8, height: 20 };
|
|
488
559
|
private readonly carets = new Set<(position: EditorPosition) => void>();
|
|
489
560
|
private readonly hovers = new Set<(hover: EditorHover | null) => void>();
|
|
561
|
+
private readonly lineHeights = new Set<Listener>();
|
|
490
562
|
private readonly views = new Set<Listener>();
|
|
491
563
|
private readonly keys = new Set<EditorKeyHandler>();
|
|
492
564
|
|
|
@@ -501,6 +573,35 @@ export class FakeEditor implements Editor {
|
|
|
501
573
|
this.inlayHints = hints;
|
|
502
574
|
}
|
|
503
575
|
|
|
576
|
+
private readonly inlayHovers = new Set<(target: EditorInlayTarget | null) => void>();
|
|
577
|
+
private readonly inlayHandlers = new Set<EditorInlayClickHandler>();
|
|
578
|
+
|
|
579
|
+
/* The hint, or part of one, the last `setInlayLink` drew as a link. */
|
|
580
|
+
inlayLink: { readonly id: string; readonly part: number | null } | null = null;
|
|
581
|
+
|
|
582
|
+
onInlayHover(listener: (target: EditorInlayTarget | null) => void): () => void {
|
|
583
|
+
return subscribe(this.inlayHovers, listener);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/* The pointer resting on a hint or a part of one, or leaving them. */
|
|
587
|
+
hoverInlay(id: string | null, part: number | null = null): void {
|
|
588
|
+
emit(this.inlayHovers, id === null ? null : { id, part, rect: { left: 0, top: 0, right: 40, bottom: 20 } });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
onInlayClick(handler: EditorInlayClickHandler): () => void {
|
|
592
|
+
return subscribe(this.inlayHandlers, handler);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* A press on a hint as the editor sees it first; true when a handler took it. */
|
|
596
|
+
clickInlay(click: Partial<EditorInlayClick> & Pick<EditorInlayClick, 'id'>): boolean {
|
|
597
|
+
const full: EditorInlayClick = { part: null, mod: false, alt: false, shift: false, clicks: 1, touch: false, ...click };
|
|
598
|
+
return [...this.inlayHandlers].some((handler) => handler(full));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
setInlayLink(target: { readonly id: string; readonly part: number | null } | null): void {
|
|
602
|
+
this.inlayLink = target;
|
|
603
|
+
}
|
|
604
|
+
|
|
504
605
|
/* The suggestion after the caret now, which any change of the text takes away as the real editor does. */
|
|
505
606
|
ghost: EditorGhostText | null = null;
|
|
506
607
|
|
|
@@ -544,6 +645,32 @@ export class FakeEditor implements Editor {
|
|
|
544
645
|
/* The selection a person made; without one the caret. */
|
|
545
646
|
selection: EditorRange | null = null;
|
|
546
647
|
|
|
648
|
+
/* A test supplies the measured height; this fake has no layout. */
|
|
649
|
+
contentHeight = 0;
|
|
650
|
+
|
|
651
|
+
setScrollTop(top: number): void {
|
|
652
|
+
if (Number.isFinite(top) && this.getScrollTop() !== Math.max(0, top)) {
|
|
653
|
+
this.scrollTop = Math.max(0, top);
|
|
654
|
+
emit(this.views);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
getLineHeight(line: number): number {
|
|
659
|
+
return Number.isInteger(line) && line >= 0 && line < this.text.split('\n').length ? this.charSize.height : 0;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
getBaseLineHeight(): number {
|
|
663
|
+
return this.charSize.height;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
onLineHeights(listener: Listener): () => void {
|
|
667
|
+
return subscribe(this.lineHeights, listener);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
getContentHeight(): number {
|
|
671
|
+
return this.contentHeight;
|
|
672
|
+
}
|
|
673
|
+
|
|
547
674
|
getSelection(): EditorRange {
|
|
548
675
|
return this.selection ?? { start: this.caret, end: this.caret };
|
|
549
676
|
}
|
|
@@ -675,7 +802,8 @@ export class FakeEditor implements Editor {
|
|
|
675
802
|
return [...this.keys].some((handler) => handler(full));
|
|
676
803
|
}
|
|
677
804
|
|
|
678
|
-
|
|
805
|
+
/* The fake keeps no undo history, so `mergeUndo` changes nothing here. It keeps no selection direction either, so a test of `mapSelections` keeping a backward selection needs the real editor. */
|
|
806
|
+
applyEdits(edits: readonly EditorContentChange[], options: EditorApplyEditsOptions = {}): boolean {
|
|
679
807
|
if (this.readOnly || edits.length === 0) {
|
|
680
808
|
return false;
|
|
681
809
|
}
|
|
@@ -702,11 +830,14 @@ export class FakeEditor implements Editor {
|
|
|
702
830
|
return offset + delta;
|
|
703
831
|
};
|
|
704
832
|
const moved = others.map((range) => ({ anchor: follow(range.anchor), head: follow(range.head) }));
|
|
705
|
-
this.
|
|
706
|
-
this.
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
833
|
+
const mapped = options.mapSelections && this.getSelections().map((range) => options.mapSelections!(range));
|
|
834
|
+
this.replaceText(text, 'command', mapped);
|
|
835
|
+
if (mapped === undefined) {
|
|
836
|
+
this.otherSelections = moved.map((range) => ({
|
|
837
|
+
start: positionIn(text, Math.min(range.anchor, range.head)),
|
|
838
|
+
end: positionIn(text, Math.max(range.anchor, range.head))
|
|
839
|
+
}));
|
|
840
|
+
}
|
|
710
841
|
emit(this.changes);
|
|
711
842
|
return true;
|
|
712
843
|
}
|
|
@@ -724,6 +855,10 @@ export class FakeEditor implements Editor {
|
|
|
724
855
|
this.readOnlyReason = reason;
|
|
725
856
|
}
|
|
726
857
|
|
|
858
|
+
isReadOnly(): boolean {
|
|
859
|
+
return this.readOnly;
|
|
860
|
+
}
|
|
861
|
+
|
|
727
862
|
/* The commands run, in order; the fake has no commands of its own to carry out. */
|
|
728
863
|
readonly commands: EditorRunCommand[] = [];
|
|
729
864
|
|
|
@@ -742,8 +877,11 @@ export class FakeEditor implements Editor {
|
|
|
742
877
|
this.textChanges.clear();
|
|
743
878
|
this.carets.clear();
|
|
744
879
|
this.hovers.clear();
|
|
880
|
+
this.inlayHovers.clear();
|
|
881
|
+
this.inlayHandlers.clear();
|
|
745
882
|
this.attributionHovers.clear();
|
|
746
883
|
this.views.clear();
|
|
884
|
+
this.lineHeights.clear();
|
|
747
885
|
this.keys.clear();
|
|
748
886
|
this.saves.clear();
|
|
749
887
|
this.blurs.clear();
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { EditorSearchError, type EditorSearchClient } from './search-client.ts';
|
|
2
|
+
|
|
3
|
+
export interface FindSearchOptions {
|
|
4
|
+
readonly createClient: () => EditorSearchClient | null;
|
|
5
|
+
readonly schedule?: (callback: () => void, delay: number) => () => void;
|
|
6
|
+
readonly now?: () => number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type SearchKind = 'live' | 'preview' | 'command';
|
|
10
|
+
|
|
11
|
+
interface SearchJob {
|
|
12
|
+
readonly kind: SearchKind;
|
|
13
|
+
readonly valid?: () => boolean;
|
|
14
|
+
readonly run: (client: EditorSearchClient, isCurrent: () => boolean) => Promise<unknown>;
|
|
15
|
+
readonly resolve: (value: unknown) => void;
|
|
16
|
+
readonly reject: (error: unknown) => void;
|
|
17
|
+
cancelled: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function scheduleFind(callback: () => void, delay: number): () => void {
|
|
21
|
+
const timer = setTimeout(callback, delay);
|
|
22
|
+
return () => clearTimeout(timer);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function superseded(): EditorSearchError {
|
|
26
|
+
return new EditorSearchError('superseded', 'The find operation is no longer current.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Commands finish atomically; short background requests reuse the worker before newer work starts. */
|
|
30
|
+
export class FindSearchQueue {
|
|
31
|
+
private client: EditorSearchClient | null | undefined;
|
|
32
|
+
private active: SearchJob | undefined;
|
|
33
|
+
private pending: SearchJob[] = [];
|
|
34
|
+
private cancelTimer: (() => void) | undefined;
|
|
35
|
+
private started = 0;
|
|
36
|
+
private disposed = false;
|
|
37
|
+
private epoch = 0;
|
|
38
|
+
private readonly options: FindSearchOptions;
|
|
39
|
+
|
|
40
|
+
constructor(options: FindSearchOptions) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get busy(): boolean {
|
|
45
|
+
return this.active !== undefined || this.pending.length > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
request<Result>(
|
|
49
|
+
run: (client: EditorSearchClient, isCurrent: () => boolean) => Promise<Result>,
|
|
50
|
+
kind: SearchKind = 'live',
|
|
51
|
+
valid?: () => boolean
|
|
52
|
+
): Promise<Result> {
|
|
53
|
+
if (this.disposed) {
|
|
54
|
+
return Promise.reject(new EditorSearchError('disposed', 'The find session was disposed.'));
|
|
55
|
+
}
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
this.pending = this.pending.filter((job) => {
|
|
58
|
+
if (job.kind !== 'command' && (job.kind === kind || (kind !== 'preview' && job.kind === 'preview'))) {
|
|
59
|
+
job.cancelled = true;
|
|
60
|
+
job.reject(superseded());
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
this.pending.push({ kind, valid, run, resolve: (value) => resolve(value as Result), reject, cancelled: false });
|
|
66
|
+
this.pending.sort((left, right) => priority(left.kind) - priority(right.kind));
|
|
67
|
+
if (this.active === undefined) {
|
|
68
|
+
this.pump();
|
|
69
|
+
} else if (
|
|
70
|
+
(kind !== 'preview' || this.active.valid?.() === false) &&
|
|
71
|
+
(this.active.kind !== 'command' || this.active.valid?.() === false) &&
|
|
72
|
+
this.cancelTimer === undefined
|
|
73
|
+
) {
|
|
74
|
+
this.cancelTimer = (this.options.schedule ?? scheduleFind)(
|
|
75
|
+
() => {
|
|
76
|
+
this.cancelTimer = undefined;
|
|
77
|
+
if (this.pending.length > 0 && this.active !== undefined && (this.active.kind !== 'command' || this.active.valid?.() === false)) {
|
|
78
|
+
this.active.cancelled = true;
|
|
79
|
+
this.active.reject(superseded());
|
|
80
|
+
this.client?.cancel();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
Math.max(0, 200 - (this.now() - this.started))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private now(): number {
|
|
90
|
+
return this.options.now?.() ?? performance.now();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private pump(): void {
|
|
94
|
+
if (this.disposed || this.active !== undefined) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const job = this.pending.shift();
|
|
98
|
+
if (job === undefined) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.active = job;
|
|
102
|
+
this.started = this.now();
|
|
103
|
+
const epoch = this.epoch;
|
|
104
|
+
const alive = (): boolean => !this.disposed && epoch === this.epoch && !job.cancelled;
|
|
105
|
+
const isCurrent = (): boolean => alive() && job.valid?.() !== false;
|
|
106
|
+
void Promise.resolve()
|
|
107
|
+
.then(() => {
|
|
108
|
+
if (!isCurrent()) {
|
|
109
|
+
throw superseded();
|
|
110
|
+
}
|
|
111
|
+
this.client ??= this.options.createClient();
|
|
112
|
+
if (this.client === null) {
|
|
113
|
+
throw new EditorSearchError('unavailable', 'Regex search needs a search worker.');
|
|
114
|
+
}
|
|
115
|
+
const client = this.client;
|
|
116
|
+
const guarded: EditorSearchClient = {
|
|
117
|
+
search: (...args) => (isCurrent() ? client.search(...args) : Promise.reject(superseded())),
|
|
118
|
+
match: (...args) => (isCurrent() ? client.match(...args) : Promise.reject(superseded())),
|
|
119
|
+
next: (...args) => (isCurrent() ? client.next(...args) : Promise.reject(superseded())),
|
|
120
|
+
replacements: (...args) => (isCurrent() ? client.replacements(...args) : Promise.reject(superseded())),
|
|
121
|
+
cancel: () => client.cancel(),
|
|
122
|
+
release: () => client.release(),
|
|
123
|
+
dispose: () => client.dispose()
|
|
124
|
+
};
|
|
125
|
+
return job.run(guarded, isCurrent);
|
|
126
|
+
})
|
|
127
|
+
.then((value) => {
|
|
128
|
+
if (alive()) {
|
|
129
|
+
job.resolve(value);
|
|
130
|
+
} else {
|
|
131
|
+
job.reject(superseded());
|
|
132
|
+
}
|
|
133
|
+
}, job.reject)
|
|
134
|
+
.finally(() => {
|
|
135
|
+
this.active = undefined;
|
|
136
|
+
this.cancelTimer?.();
|
|
137
|
+
this.cancelTimer = undefined;
|
|
138
|
+
this.pump();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
release(): void {
|
|
143
|
+
this.epoch++;
|
|
144
|
+
this.active?.reject(superseded());
|
|
145
|
+
for (const job of this.pending) {
|
|
146
|
+
job.cancelled = true;
|
|
147
|
+
job.reject(superseded());
|
|
148
|
+
}
|
|
149
|
+
this.pending = [];
|
|
150
|
+
this.cancelTimer?.();
|
|
151
|
+
this.cancelTimer = undefined;
|
|
152
|
+
this.client?.release();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
dispose(): void {
|
|
156
|
+
this.disposed = true;
|
|
157
|
+
this.release();
|
|
158
|
+
this.client?.dispose();
|
|
159
|
+
this.client = undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function priority(kind: SearchKind): number {
|
|
164
|
+
return kind === 'command' ? 0 : kind === 'live' ? 1 : 2;
|
|
165
|
+
}
|