@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.
- 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 +189 -41
- package/dist/fake.d.ts +45 -5
- package/dist/fake.js +121 -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 +86 -2
- package/dist/uniform-geometry.d.ts +7 -0
- package/dist/uniform-geometry.js +49 -0
- package/dist/view.d.ts +69 -17
- package/dist/view.js +427 -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 +222 -42
- package/src/fake.ts +157 -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 +178 -8
- package/src/uniform-geometry.ts +57 -0
- package/src/view.ts +505 -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 {
|
|
@@ -438,6 +509,7 @@ export class FakeEditor implements Editor {
|
|
|
438
509
|
smartKeys: Partial<EditorSmartKeys>;
|
|
439
510
|
|
|
440
511
|
guides = true;
|
|
512
|
+
stickyScroll = true;
|
|
441
513
|
whitespace = false;
|
|
442
514
|
rightMargin: number | null = null;
|
|
443
515
|
|
|
@@ -445,6 +517,10 @@ export class FakeEditor implements Editor {
|
|
|
445
517
|
this.guides = guides;
|
|
446
518
|
}
|
|
447
519
|
|
|
520
|
+
setStickyScroll(sticky: boolean): void {
|
|
521
|
+
this.stickyScroll = sticky;
|
|
522
|
+
}
|
|
523
|
+
|
|
448
524
|
setWhitespace(whitespace: boolean): void {
|
|
449
525
|
this.whitespace = whitespace;
|
|
450
526
|
}
|
|
@@ -487,6 +563,7 @@ export class FakeEditor implements Editor {
|
|
|
487
563
|
charSize = { width: 8, height: 20 };
|
|
488
564
|
private readonly carets = new Set<(position: EditorPosition) => void>();
|
|
489
565
|
private readonly hovers = new Set<(hover: EditorHover | null) => void>();
|
|
566
|
+
private readonly lineHeights = new Set<Listener>();
|
|
490
567
|
private readonly views = new Set<Listener>();
|
|
491
568
|
private readonly keys = new Set<EditorKeyHandler>();
|
|
492
569
|
|
|
@@ -501,6 +578,35 @@ export class FakeEditor implements Editor {
|
|
|
501
578
|
this.inlayHints = hints;
|
|
502
579
|
}
|
|
503
580
|
|
|
581
|
+
private readonly inlayHovers = new Set<(target: EditorInlayTarget | null) => void>();
|
|
582
|
+
private readonly inlayHandlers = new Set<EditorInlayClickHandler>();
|
|
583
|
+
|
|
584
|
+
/* The hint, or part of one, the last `setInlayLink` drew as a link. */
|
|
585
|
+
inlayLink: { readonly id: string; readonly part: number | null } | null = null;
|
|
586
|
+
|
|
587
|
+
onInlayHover(listener: (target: EditorInlayTarget | null) => void): () => void {
|
|
588
|
+
return subscribe(this.inlayHovers, listener);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* The pointer resting on a hint or a part of one, or leaving them. */
|
|
592
|
+
hoverInlay(id: string | null, part: number | null = null): void {
|
|
593
|
+
emit(this.inlayHovers, id === null ? null : { id, part, rect: { left: 0, top: 0, right: 40, bottom: 20 } });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
onInlayClick(handler: EditorInlayClickHandler): () => void {
|
|
597
|
+
return subscribe(this.inlayHandlers, handler);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* A press on a hint as the editor sees it first; true when a handler took it. */
|
|
601
|
+
clickInlay(click: Partial<EditorInlayClick> & Pick<EditorInlayClick, 'id'>): boolean {
|
|
602
|
+
const full: EditorInlayClick = { part: null, mod: false, alt: false, shift: false, clicks: 1, touch: false, ...click };
|
|
603
|
+
return [...this.inlayHandlers].some((handler) => handler(full));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
setInlayLink(target: { readonly id: string; readonly part: number | null } | null): void {
|
|
607
|
+
this.inlayLink = target;
|
|
608
|
+
}
|
|
609
|
+
|
|
504
610
|
/* The suggestion after the caret now, which any change of the text takes away as the real editor does. */
|
|
505
611
|
ghost: EditorGhostText | null = null;
|
|
506
612
|
|
|
@@ -544,6 +650,32 @@ export class FakeEditor implements Editor {
|
|
|
544
650
|
/* The selection a person made; without one the caret. */
|
|
545
651
|
selection: EditorRange | null = null;
|
|
546
652
|
|
|
653
|
+
/* A test supplies the measured height; this fake has no layout. */
|
|
654
|
+
contentHeight = 0;
|
|
655
|
+
|
|
656
|
+
setScrollTop(top: number): void {
|
|
657
|
+
if (Number.isFinite(top) && this.getScrollTop() !== Math.max(0, top)) {
|
|
658
|
+
this.scrollTop = Math.max(0, top);
|
|
659
|
+
emit(this.views);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
getLineHeight(line: number): number {
|
|
664
|
+
return Number.isInteger(line) && line >= 0 && line < this.text.split('\n').length ? this.charSize.height : 0;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
getBaseLineHeight(): number {
|
|
668
|
+
return this.charSize.height;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
onLineHeights(listener: Listener): () => void {
|
|
672
|
+
return subscribe(this.lineHeights, listener);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
getContentHeight(): number {
|
|
676
|
+
return this.contentHeight;
|
|
677
|
+
}
|
|
678
|
+
|
|
547
679
|
getSelection(): EditorRange {
|
|
548
680
|
return this.selection ?? { start: this.caret, end: this.caret };
|
|
549
681
|
}
|
|
@@ -675,7 +807,8 @@ export class FakeEditor implements Editor {
|
|
|
675
807
|
return [...this.keys].some((handler) => handler(full));
|
|
676
808
|
}
|
|
677
809
|
|
|
678
|
-
|
|
810
|
+
/* 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. */
|
|
811
|
+
applyEdits(edits: readonly EditorContentChange[], options: EditorApplyEditsOptions = {}): boolean {
|
|
679
812
|
if (this.readOnly || edits.length === 0) {
|
|
680
813
|
return false;
|
|
681
814
|
}
|
|
@@ -702,11 +835,14 @@ export class FakeEditor implements Editor {
|
|
|
702
835
|
return offset + delta;
|
|
703
836
|
};
|
|
704
837
|
const moved = others.map((range) => ({ anchor: follow(range.anchor), head: follow(range.head) }));
|
|
705
|
-
this.
|
|
706
|
-
this.
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
838
|
+
const mapped = options.mapSelections && this.getSelections().map((range) => options.mapSelections!(range));
|
|
839
|
+
this.replaceText(text, 'command', mapped);
|
|
840
|
+
if (mapped === undefined) {
|
|
841
|
+
this.otherSelections = moved.map((range) => ({
|
|
842
|
+
start: positionIn(text, Math.min(range.anchor, range.head)),
|
|
843
|
+
end: positionIn(text, Math.max(range.anchor, range.head))
|
|
844
|
+
}));
|
|
845
|
+
}
|
|
710
846
|
emit(this.changes);
|
|
711
847
|
return true;
|
|
712
848
|
}
|
|
@@ -724,6 +860,10 @@ export class FakeEditor implements Editor {
|
|
|
724
860
|
this.readOnlyReason = reason;
|
|
725
861
|
}
|
|
726
862
|
|
|
863
|
+
isReadOnly(): boolean {
|
|
864
|
+
return this.readOnly;
|
|
865
|
+
}
|
|
866
|
+
|
|
727
867
|
/* The commands run, in order; the fake has no commands of its own to carry out. */
|
|
728
868
|
readonly commands: EditorRunCommand[] = [];
|
|
729
869
|
|
|
@@ -742,8 +882,11 @@ export class FakeEditor implements Editor {
|
|
|
742
882
|
this.textChanges.clear();
|
|
743
883
|
this.carets.clear();
|
|
744
884
|
this.hovers.clear();
|
|
885
|
+
this.inlayHovers.clear();
|
|
886
|
+
this.inlayHandlers.clear();
|
|
745
887
|
this.attributionHovers.clear();
|
|
746
888
|
this.views.clear();
|
|
889
|
+
this.lineHeights.clear();
|
|
747
890
|
this.keys.clear();
|
|
748
891
|
this.saves.clear();
|
|
749
892
|
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
|
+
}
|