@adecore/editor 0.38.0 → 0.40.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/controller.d.ts +1 -0
- package/dist/controller.js +17 -28
- package/dist/engine.js +2 -9
- package/dist/find.js +12 -13
- package/dist/layout.js +7 -2
- package/dist/paint.d.ts +2 -0
- package/dist/paint.js +23 -20
- package/dist/view.d.ts +3 -2
- package/dist/view.js +46 -59
- package/package.json +6 -6
- package/src/attribution.ts +14 -0
- package/src/code-block.ts +25 -0
- package/src/column-selection.ts +10 -0
- package/src/controller.ts +189 -28
- package/src/engine.ts +87 -9
- package/src/fake.ts +79 -0
- package/src/find-search.ts +20 -0
- package/src/find.ts +135 -14
- package/src/geometry-stops.ts +10 -0
- package/src/grammar-preparation.ts +23 -0
- package/src/index.ts +1 -0
- package/src/input.ts +13 -0
- package/src/keymap-table.ts +9 -0
- package/src/keymap.ts +25 -0
- package/src/layout-changes.ts +7 -0
- package/src/layout.ts +219 -3
- package/src/lens.ts +3 -0
- package/src/listeners.ts +1 -0
- package/src/metrics.ts +14 -0
- package/src/modifier-gesture.ts +17 -0
- package/src/native-input.ts +27 -0
- package/src/offsets.ts +10 -0
- package/src/outline.ts +38 -0
- package/src/overview.ts +13 -0
- package/src/paint.ts +140 -20
- package/src/php-html.ts +1 -0
- package/src/pointer.ts +50 -0
- package/src/scroll-animation.ts +10 -0
- package/src/scroll.ts +7 -0
- package/src/search-client.ts +32 -0
- package/src/search-worker.ts +38 -0
- package/src/semantic.ts +12 -0
- package/src/shiki-document.ts +19 -0
- package/src/shiki-worker.ts +68 -0
- package/src/shiki.ts +37 -0
- package/src/testing.ts +17 -0
- package/src/theme-scopes.ts +14 -0
- package/src/tokens.ts +31 -0
- package/src/touch.ts +16 -0
- package/src/tracked-range.ts +5 -0
- package/src/uniform-geometry.ts +10 -0
- package/src/view.ts +481 -75
- package/src/worker-client.ts +54 -0
package/src/fake.ts
CHANGED
|
@@ -77,11 +77,14 @@ const LINE_BREAK = /\r\n|\r|\n/g;
|
|
|
77
77
|
function linesOf(text: string): { start: number; end: number }[] {
|
|
78
78
|
const lines: { start: number; end: number }[] = [];
|
|
79
79
|
let start = 0;
|
|
80
|
+
|
|
80
81
|
for (const match of text.matchAll(LINE_BREAK)) {
|
|
81
82
|
lines.push({ start, end: match.index });
|
|
82
83
|
start = match.index + match[0].length;
|
|
83
84
|
}
|
|
85
|
+
|
|
84
86
|
lines.push({ start, end: text.length });
|
|
87
|
+
|
|
85
88
|
return lines;
|
|
86
89
|
}
|
|
87
90
|
|
|
@@ -98,6 +101,7 @@ function clampOffset(text: string, value: number): number {
|
|
|
98
101
|
const offset = Math.max(0, Math.min(text.length, Number.isFinite(value) ? Math.trunc(value) : 0));
|
|
99
102
|
const splitsBreak = text[offset - 1] === '\r' && text[offset] === '\n';
|
|
100
103
|
const splitsPair = isHighSurrogate(text.charCodeAt(offset - 1)) && isLowSurrogate(text.charCodeAt(offset));
|
|
104
|
+
|
|
101
105
|
return splitsBreak || splitsPair ? offset - 1 : offset;
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -105,9 +109,11 @@ function positionIn(text: string, offset: number): EditorPosition {
|
|
|
105
109
|
const at = clampOffset(text, offset);
|
|
106
110
|
const lines = linesOf(text);
|
|
107
111
|
let line = lines.length - 1;
|
|
112
|
+
|
|
108
113
|
while (line > 0 && lines[line]!.start > at) {
|
|
109
114
|
line--;
|
|
110
115
|
}
|
|
116
|
+
|
|
111
117
|
return { line, character: Math.min(at, lines[line]!.end) - lines[line]!.start };
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -115,28 +121,36 @@ function offsetIn(text: string, position: EditorPosition): number {
|
|
|
115
121
|
const lines = linesOf(text);
|
|
116
122
|
const line = lines[Math.max(0, Math.min(Number.isFinite(position.line) ? Math.trunc(position.line) : 0, lines.length - 1))]!;
|
|
117
123
|
const character = Math.max(0, Math.min(Number.isFinite(position.character) ? Math.trunc(position.character) : 0, line.end - line.start));
|
|
124
|
+
|
|
118
125
|
return clampOffset(text, line.start + character);
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
/* The line of a one-based line number, clamped to the text. */
|
|
122
129
|
function lineNumbered(text: string, line: number): { start: number; end: number } {
|
|
123
130
|
const lines = linesOf(text);
|
|
131
|
+
|
|
124
132
|
return lines[Math.min(Math.max(1, Math.trunc(line) || 1), lines.length) - 1]!;
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
/* Clamps every selection and merges those that overlap or touch, each merge taking the place and direction of the last given of them, as the real document does. */
|
|
128
136
|
function normalized(text: string, selections: readonly OffsetSelection[]): OffsetSelection[] {
|
|
129
137
|
const input = selections.length > 0 ? selections : [{ anchor: 0, head: 0 }];
|
|
138
|
+
|
|
130
139
|
const clamped = input.map((raw) => {
|
|
131
140
|
const anchor = clampOffset(text, raw.anchor);
|
|
141
|
+
|
|
132
142
|
return { anchor, head: raw.head === raw.anchor ? anchor : clampOffset(text, raw.head) };
|
|
133
143
|
});
|
|
144
|
+
|
|
134
145
|
const order = clamped
|
|
135
146
|
.map((selection, index) => ({ from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head), index }))
|
|
136
147
|
.sort((left, right) => left.from - right.from);
|
|
148
|
+
|
|
137
149
|
const groups: { from: number; to: number; last: number }[] = [];
|
|
150
|
+
|
|
138
151
|
for (const { from, to, index } of order) {
|
|
139
152
|
const group = groups.at(-1);
|
|
153
|
+
|
|
140
154
|
if (group !== undefined && from <= group.to) {
|
|
141
155
|
group.to = Math.max(group.to, to);
|
|
142
156
|
group.last = Math.max(group.last, index);
|
|
@@ -144,10 +158,12 @@ function normalized(text: string, selections: readonly OffsetSelection[]): Offse
|
|
|
144
158
|
groups.push({ from, to, last: index });
|
|
145
159
|
}
|
|
146
160
|
}
|
|
161
|
+
|
|
147
162
|
return groups
|
|
148
163
|
.sort((left, right) => left.last - right.last)
|
|
149
164
|
.map((group) => {
|
|
150
165
|
const selection = clamped[group.last]!;
|
|
166
|
+
|
|
151
167
|
return selection.anchor > selection.head ? { anchor: group.to, head: group.from } : { anchor: group.from, head: group.to };
|
|
152
168
|
});
|
|
153
169
|
}
|
|
@@ -156,28 +172,36 @@ function normalized(text: string, selections: readonly OffsetSelection[]): Offse
|
|
|
156
172
|
function orderedEdits(edits: readonly Edit[]): Edit[] | null {
|
|
157
173
|
const sorted = [...edits].sort((left, right) => left.from - right.from || left.to - right.to);
|
|
158
174
|
const unique: Edit[] = [];
|
|
175
|
+
|
|
159
176
|
for (const edit of sorted) {
|
|
160
177
|
const previous = unique.at(-1);
|
|
178
|
+
|
|
161
179
|
if (previous !== undefined && edit.from === previous.from && edit.to === previous.to && edit.text === previous.text) {
|
|
162
180
|
continue;
|
|
163
181
|
}
|
|
182
|
+
|
|
164
183
|
if (previous !== undefined && (edit.from < previous.to || (edit.from === previous.from && edit.to === previous.to))) {
|
|
165
184
|
return null;
|
|
166
185
|
}
|
|
186
|
+
|
|
167
187
|
unique.push(edit);
|
|
168
188
|
}
|
|
189
|
+
|
|
169
190
|
return unique;
|
|
170
191
|
}
|
|
171
192
|
|
|
172
193
|
/* Where an offset lands after ordered edits: one inside a replaced stretch, or at an insertion, ends up behind the new text. */
|
|
173
194
|
function mapOffset(offset: number, edits: readonly Edit[]): number {
|
|
174
195
|
let delta = 0;
|
|
196
|
+
|
|
175
197
|
for (const edit of edits) {
|
|
176
198
|
if (edit.to > offset) {
|
|
177
199
|
return edit.from <= offset ? edit.from + delta + edit.text.length : offset + delta;
|
|
178
200
|
}
|
|
201
|
+
|
|
179
202
|
delta += edit.text.length - (edit.to - edit.from);
|
|
180
203
|
}
|
|
204
|
+
|
|
181
205
|
return offset + delta;
|
|
182
206
|
}
|
|
183
207
|
|
|
@@ -233,6 +257,7 @@ export class FakeEditor implements Editor {
|
|
|
233
257
|
this.revealedLine = options.line ?? null;
|
|
234
258
|
this.column = options.column ?? null;
|
|
235
259
|
this.scrollTop = options.scrollTop ?? null;
|
|
260
|
+
|
|
236
261
|
if (options.line !== undefined) {
|
|
237
262
|
const line = lineNumbered(this.text, options.line);
|
|
238
263
|
const offset = Math.min(line.end, line.start + Math.max(0, (options.column ?? 1) - 1));
|
|
@@ -245,14 +270,18 @@ export class FakeEditor implements Editor {
|
|
|
245
270
|
type(text: string): void {
|
|
246
271
|
this.assertLive();
|
|
247
272
|
const span = changedSpan(this.text, text);
|
|
273
|
+
|
|
248
274
|
if (this.readOnly || span === null) {
|
|
249
275
|
return;
|
|
250
276
|
}
|
|
277
|
+
|
|
251
278
|
const edit = { from: span.start, to: span.end, text: span.text };
|
|
252
279
|
const end = span.start + span.text.length;
|
|
280
|
+
|
|
253
281
|
const others = this.selections
|
|
254
282
|
.slice(0, -1)
|
|
255
283
|
.map((selection) => ({ anchor: mapOffset(selection.anchor, [edit]), head: mapOffset(selection.head, [edit]) }));
|
|
284
|
+
|
|
256
285
|
this.edit([edit], 'input', false, () => [...others, { anchor: end, head: end }]);
|
|
257
286
|
}
|
|
258
287
|
|
|
@@ -292,41 +321,53 @@ export class FakeEditor implements Editor {
|
|
|
292
321
|
place?: (text: string) => readonly OffsetSelection[]
|
|
293
322
|
): boolean {
|
|
294
323
|
const before = this.state();
|
|
324
|
+
|
|
295
325
|
if (!this.rewrite(edits, source, place)) {
|
|
296
326
|
return false;
|
|
297
327
|
}
|
|
328
|
+
|
|
298
329
|
const last = this.undoStack.at(-1);
|
|
330
|
+
|
|
299
331
|
if (mergeUndo && last !== undefined && last.after.text === before.text) {
|
|
300
332
|
last.after = this.state();
|
|
301
333
|
} else {
|
|
302
334
|
this.undoStack.push({ before, after: this.state() });
|
|
303
335
|
}
|
|
336
|
+
|
|
304
337
|
this.redoStack = [];
|
|
338
|
+
|
|
305
339
|
if (source !== 'external') {
|
|
306
340
|
emit(this.changes);
|
|
307
341
|
}
|
|
342
|
+
|
|
308
343
|
return true;
|
|
309
344
|
}
|
|
310
345
|
|
|
311
346
|
private rewrite(edits: readonly Edit[], source: EditorTextChange['source'], place?: (text: string) => readonly OffsetSelection[]): boolean {
|
|
312
347
|
const before = this.text;
|
|
313
348
|
let text = before;
|
|
349
|
+
|
|
314
350
|
for (let index = edits.length - 1; index >= 0; index--) {
|
|
315
351
|
const edit = edits[index]!;
|
|
316
352
|
text = text.slice(0, edit.from) + edit.text + text.slice(edit.to);
|
|
317
353
|
}
|
|
354
|
+
|
|
318
355
|
if (text === before) {
|
|
319
356
|
return false;
|
|
320
357
|
}
|
|
358
|
+
|
|
321
359
|
const changed = edits.filter((edit) => before.slice(edit.from, edit.to) !== edit.text);
|
|
322
360
|
this.text = text;
|
|
361
|
+
|
|
323
362
|
this.selections = normalized(
|
|
324
363
|
text,
|
|
325
364
|
place?.(text) ?? this.selections.map((selection) => ({ anchor: mapOffset(selection.anchor, edits), head: mapOffset(selection.head, edits) }))
|
|
326
365
|
);
|
|
366
|
+
|
|
327
367
|
this.followTracked(changed);
|
|
328
368
|
this.ghost = null;
|
|
329
369
|
this.announceCaret();
|
|
370
|
+
|
|
330
371
|
// Last to first, so each change's range is also where it stands once the ones after it are applied, as the real editor reports them.
|
|
331
372
|
const change: EditorTextChange = {
|
|
332
373
|
source,
|
|
@@ -334,7 +375,9 @@ export class FakeEditor implements Editor {
|
|
|
334
375
|
.reverse()
|
|
335
376
|
.map((edit) => ({ range: { start: positionIn(before, edit.from), end: positionIn(before, edit.to) }, text: edit.text }))
|
|
336
377
|
};
|
|
378
|
+
|
|
337
379
|
emit(this.textChanges, change);
|
|
380
|
+
|
|
338
381
|
return true;
|
|
339
382
|
}
|
|
340
383
|
|
|
@@ -346,24 +389,30 @@ export class FakeEditor implements Editor {
|
|
|
346
389
|
private restore(direction: 'undo' | 'redo'): boolean {
|
|
347
390
|
const [from, to] = direction === 'undo' ? [this.undoStack, this.redoStack] : [this.redoStack, this.undoStack];
|
|
348
391
|
const entry = this.readOnly ? undefined : from.pop();
|
|
392
|
+
|
|
349
393
|
if (entry === undefined) {
|
|
350
394
|
return false;
|
|
351
395
|
}
|
|
396
|
+
|
|
352
397
|
to.push(entry);
|
|
353
398
|
const state = direction === 'undo' ? entry.before : entry.after;
|
|
354
399
|
const span = changedSpan(this.text, state.text);
|
|
400
|
+
|
|
355
401
|
if (span === null) {
|
|
356
402
|
this.select(state.selections);
|
|
357
403
|
} else {
|
|
358
404
|
this.rewrite([{ from: span.start, to: span.end, text: span.text }], 'command', () => state.selections);
|
|
359
405
|
}
|
|
406
|
+
|
|
360
407
|
emit(this.changes);
|
|
408
|
+
|
|
361
409
|
return true;
|
|
362
410
|
}
|
|
363
411
|
|
|
364
412
|
/* Tells the caret listeners where the primary caret is, when it moved since they last heard. */
|
|
365
413
|
private announceCaret(): void {
|
|
366
414
|
const head = this.selections.at(-1)!.head;
|
|
415
|
+
|
|
367
416
|
if (head !== this.caretOffset) {
|
|
368
417
|
this.caretOffset = head;
|
|
369
418
|
emit(this.carets, positionIn(this.text, head));
|
|
@@ -374,8 +423,10 @@ export class FakeEditor implements Editor {
|
|
|
374
423
|
|
|
375
424
|
private followTracked(edits: readonly Edit[]): void {
|
|
376
425
|
const changes = edits.map((edit) => ({ from: edit.from, to: edit.to, insertedLength: edit.text.length }));
|
|
426
|
+
|
|
377
427
|
for (const entry of this.tracked) {
|
|
378
428
|
const mapped = mapTrackedRange(entry.from, entry.to, changes);
|
|
429
|
+
|
|
379
430
|
if (mapped === null) {
|
|
380
431
|
this.tracked.delete(entry);
|
|
381
432
|
entry.lost = true;
|
|
@@ -388,6 +439,7 @@ export class FakeEditor implements Editor {
|
|
|
388
439
|
trackRange(range: EditorRange): EditorTrackedRange {
|
|
389
440
|
const entry = { from: this.offsetAt(range.start), to: this.offsetAt(range.end), lost: false };
|
|
390
441
|
this.tracked.add(entry);
|
|
442
|
+
|
|
391
443
|
return {
|
|
392
444
|
get: () => (entry.lost ? null : { start: this.positionAt(entry.from), end: this.positionAt(entry.to) }),
|
|
393
445
|
dispose: () => {
|
|
@@ -458,12 +510,14 @@ export class FakeEditor implements Editor {
|
|
|
458
510
|
replace(replacement: string, options: EditorReplaceOptions = {}): boolean {
|
|
459
511
|
this.replacements.push(replacement);
|
|
460
512
|
this.replaceOptions.push(options);
|
|
513
|
+
|
|
461
514
|
return !this.readOnly && this.findState.count > 0;
|
|
462
515
|
}
|
|
463
516
|
|
|
464
517
|
replaceAll(replacement: string, options: EditorReplaceOptions = {}): number {
|
|
465
518
|
this.replacementsOfAll.push(replacement);
|
|
466
519
|
this.replaceOptions.push(options);
|
|
520
|
+
|
|
467
521
|
return this.readOnly ? 0 : this.findState.count;
|
|
468
522
|
}
|
|
469
523
|
|
|
@@ -478,16 +532,19 @@ export class FakeEditor implements Editor {
|
|
|
478
532
|
selectFindMatches(): number {
|
|
479
533
|
this.selectedMatches = this.findState.count;
|
|
480
534
|
this.find(null);
|
|
535
|
+
|
|
481
536
|
return this.selectedMatches;
|
|
482
537
|
}
|
|
483
538
|
|
|
484
539
|
findFromCursor(query: EditorFindQuery, direction: 1 | -1): boolean {
|
|
485
540
|
this.lastFindFromCursor = { query, direction };
|
|
541
|
+
|
|
486
542
|
return true;
|
|
487
543
|
}
|
|
488
544
|
|
|
489
545
|
findStep(direction: 1 | -1): void {
|
|
490
546
|
const { count, current } = this.findState;
|
|
547
|
+
|
|
491
548
|
if (count > 0) {
|
|
492
549
|
this.announceFind({ count, current: ((current ?? 0) + direction + count) % count });
|
|
493
550
|
}
|
|
@@ -597,10 +654,12 @@ export class FakeEditor implements Editor {
|
|
|
597
654
|
/* The pointer on the bar of a mark, or leaving it. The rect is where `rectAt` puts the mark's first line unless given. */
|
|
598
655
|
hoverAttribution(id: string | null, rect?: EditorRect): void {
|
|
599
656
|
const mark = id === null ? undefined : this.attributionMarks.find((candidate) => candidate.id === id);
|
|
657
|
+
|
|
600
658
|
const hover =
|
|
601
659
|
mark === undefined
|
|
602
660
|
? null
|
|
603
661
|
: { id: mark.id, rect: rect ?? this.rectAt({ line: mark.startLine - 1, character: 0 }) ?? { left: 0, top: 0, right: 0, bottom: 0 } };
|
|
662
|
+
|
|
604
663
|
emit(this.attributionHovers, hover);
|
|
605
664
|
}
|
|
606
665
|
|
|
@@ -619,15 +678,19 @@ export class FakeEditor implements Editor {
|
|
|
619
678
|
if (widgets.length === 0) {
|
|
620
679
|
this.widgetsByOwner.delete(owner);
|
|
621
680
|
this.drawnWidgets.delete(owner);
|
|
681
|
+
|
|
622
682
|
return;
|
|
623
683
|
}
|
|
684
|
+
|
|
624
685
|
this.widgetsByOwner.set(owner, widgets);
|
|
625
686
|
const before = this.drawnWidgets.get(owner) ?? new Set<string>();
|
|
687
|
+
|
|
626
688
|
for (const widget of widgets) {
|
|
627
689
|
if (!before.has(widget.id)) {
|
|
628
690
|
widget.render(this.element.ownerDocument?.createElement('div') ?? ({} as HTMLElement));
|
|
629
691
|
}
|
|
630
692
|
}
|
|
693
|
+
|
|
631
694
|
this.drawnWidgets.set(owner, new Set(widgets.map((widget) => widget.id)));
|
|
632
695
|
}
|
|
633
696
|
|
|
@@ -804,6 +867,7 @@ export class FakeEditor implements Editor {
|
|
|
804
867
|
/* A press on a hint as the editor sees it first; true when a handler took it. */
|
|
805
868
|
clickInlay(click: Partial<EditorInlayClick> & Pick<EditorInlayClick, 'id'>): boolean {
|
|
806
869
|
const full: EditorInlayClick = { part: null, mod: false, alt: false, shift: false, clicks: 1, touch: false, ...click };
|
|
870
|
+
|
|
807
871
|
return [...this.inlayHandlers].some((handler) => handler(full));
|
|
808
872
|
}
|
|
809
873
|
|
|
@@ -864,6 +928,7 @@ export class FakeEditor implements Editor {
|
|
|
864
928
|
/* The primary selection, or null when it is only a caret; setting it tells no listener. */
|
|
865
929
|
get selection(): EditorRange | null {
|
|
866
930
|
const primary = this.selections.at(-1)!;
|
|
931
|
+
|
|
867
932
|
return primary.anchor === primary.head ? null : rangeIn(this.text, primary);
|
|
868
933
|
}
|
|
869
934
|
|
|
@@ -920,6 +985,7 @@ export class FakeEditor implements Editor {
|
|
|
920
985
|
if (ranges.length === 0) {
|
|
921
986
|
return;
|
|
922
987
|
}
|
|
988
|
+
|
|
923
989
|
this.lastReveal = reveal;
|
|
924
990
|
this.select(ranges.map((range) => ({ anchor: this.offsetAt(range.start), head: this.offsetAt(range.end) })));
|
|
925
991
|
}
|
|
@@ -970,6 +1036,7 @@ export class FakeEditor implements Editor {
|
|
|
970
1036
|
rectAt(position: EditorPosition): EditorRect | null {
|
|
971
1037
|
const left = this.origin.left + position.character * this.charSize.width;
|
|
972
1038
|
const top = this.origin.top + position.line * this.charSize.height;
|
|
1039
|
+
|
|
973
1040
|
return { left, top, right: left + this.charSize.width, bottom: top + this.charSize.height };
|
|
974
1041
|
}
|
|
975
1042
|
|
|
@@ -989,6 +1056,7 @@ export class FakeEditor implements Editor {
|
|
|
989
1056
|
if (range !== undefined) {
|
|
990
1057
|
this.visibleRange = range;
|
|
991
1058
|
}
|
|
1059
|
+
|
|
992
1060
|
emit(this.views);
|
|
993
1061
|
}
|
|
994
1062
|
|
|
@@ -1027,18 +1095,23 @@ export class FakeEditor implements Editor {
|
|
|
1027
1095
|
/* A key as the editor sees it first; true when a handler took it. */
|
|
1028
1096
|
press(event: Pick<KeyboardEvent, 'key'> & Partial<KeyboardEvent>): boolean {
|
|
1029
1097
|
const full = { ctrlKey: false, metaKey: false, altKey: false, shiftKey: false, preventDefault: () => undefined, ...event } as KeyboardEvent;
|
|
1098
|
+
|
|
1030
1099
|
return [...this.keys].some((handler) => handler(full));
|
|
1031
1100
|
}
|
|
1032
1101
|
|
|
1033
1102
|
applyEdits(edits: readonly EditorContentChange[], options: EditorApplyEditsOptions = {}): boolean {
|
|
1034
1103
|
const ordered = orderedEdits(edits.map((edit) => ({ from: this.offsetAt(edit.range.start), to: this.offsetAt(edit.range.end), text: edit.text })));
|
|
1104
|
+
|
|
1035
1105
|
if (this.readOnly || ordered === null || ordered.length === 0) {
|
|
1036
1106
|
return false;
|
|
1037
1107
|
}
|
|
1108
|
+
|
|
1038
1109
|
const { mapSelections } = options;
|
|
1110
|
+
|
|
1039
1111
|
const mapped =
|
|
1040
1112
|
mapSelections &&
|
|
1041
1113
|
this.selections.map((selection) => ({ backward: selection.head < selection.anchor, range: mapSelections(rangeIn(this.text, selection)) }));
|
|
1114
|
+
|
|
1042
1115
|
// A backward selection stays backward, as the real editor places it.
|
|
1043
1116
|
const place =
|
|
1044
1117
|
mapped &&
|
|
@@ -1046,8 +1119,10 @@ export class FakeEditor implements Editor {
|
|
|
1046
1119
|
mapped.map(({ backward, range }) => {
|
|
1047
1120
|
const from = offsetIn(text, range.start);
|
|
1048
1121
|
const to = offsetIn(text, range.end);
|
|
1122
|
+
|
|
1049
1123
|
return backward ? { anchor: to, head: from } : { anchor: from, head: to };
|
|
1050
1124
|
}));
|
|
1125
|
+
|
|
1051
1126
|
return this.edit(ordered, 'command', options.mergeUndo === true, place);
|
|
1052
1127
|
}
|
|
1053
1128
|
|
|
@@ -1073,6 +1148,7 @@ export class FakeEditor implements Editor {
|
|
|
1073
1148
|
|
|
1074
1149
|
runCommand(command: EditorRunCommand): boolean {
|
|
1075
1150
|
this.commands.push(command);
|
|
1151
|
+
|
|
1076
1152
|
return command === 'undo' || command === 'redo' ? this.restore(command) : false;
|
|
1077
1153
|
}
|
|
1078
1154
|
|
|
@@ -1118,15 +1194,18 @@ export class FakeEditorEngine implements EditorEngine {
|
|
|
1118
1194
|
mount(element: HTMLElement, options: EditorOptions): FakeEditor {
|
|
1119
1195
|
const editor = new FakeEditor(element, options);
|
|
1120
1196
|
this.editors.push(editor);
|
|
1197
|
+
|
|
1121
1198
|
return editor;
|
|
1122
1199
|
}
|
|
1123
1200
|
|
|
1124
1201
|
/* The editor mounted last, which is the one a test of a single surface drives. */
|
|
1125
1202
|
get last(): FakeEditor {
|
|
1126
1203
|
const editor = this.editors.at(-1);
|
|
1204
|
+
|
|
1127
1205
|
if (!editor) {
|
|
1128
1206
|
throw new Error('No editor was mounted');
|
|
1129
1207
|
}
|
|
1208
|
+
|
|
1130
1209
|
return editor;
|
|
1131
1210
|
}
|
|
1132
1211
|
}
|
package/src/find-search.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface SearchJob {
|
|
|
19
19
|
|
|
20
20
|
export function scheduleFind(callback: () => void, delay: number): () => void {
|
|
21
21
|
const timer = setTimeout(callback, delay);
|
|
22
|
+
|
|
22
23
|
return () => clearTimeout(timer);
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -53,17 +54,22 @@ export class FindSearchQueue {
|
|
|
53
54
|
if (this.disposed) {
|
|
54
55
|
return Promise.reject(new EditorSearchError('disposed', 'The find session was disposed.'));
|
|
55
56
|
}
|
|
57
|
+
|
|
56
58
|
return new Promise((resolve, reject) => {
|
|
57
59
|
this.pending = this.pending.filter((job) => {
|
|
58
60
|
if (job.kind !== 'command' && (job.kind === kind || (kind !== 'preview' && job.kind === 'preview'))) {
|
|
59
61
|
job.cancelled = true;
|
|
60
62
|
job.reject(superseded());
|
|
63
|
+
|
|
61
64
|
return false;
|
|
62
65
|
}
|
|
66
|
+
|
|
63
67
|
return true;
|
|
64
68
|
});
|
|
69
|
+
|
|
65
70
|
this.pending.push({ kind, valid, run, resolve: (value) => resolve(value as Result), reject, cancelled: false });
|
|
66
71
|
this.pending.sort((left, right) => priority(left.kind) - priority(right.kind));
|
|
72
|
+
|
|
67
73
|
if (this.active === undefined) {
|
|
68
74
|
this.pump();
|
|
69
75
|
} else if (
|
|
@@ -74,6 +80,7 @@ export class FindSearchQueue {
|
|
|
74
80
|
this.cancelTimer = (this.options.schedule ?? scheduleFind)(
|
|
75
81
|
() => {
|
|
76
82
|
this.cancelTimer = undefined;
|
|
83
|
+
|
|
77
84
|
if (this.pending.length > 0 && this.active !== undefined && (this.active.kind !== 'command' || this.active.valid?.() === false)) {
|
|
78
85
|
this.active.cancelled = true;
|
|
79
86
|
this.active.reject(superseded());
|
|
@@ -94,25 +101,35 @@ export class FindSearchQueue {
|
|
|
94
101
|
if (this.disposed || this.active !== undefined) {
|
|
95
102
|
return;
|
|
96
103
|
}
|
|
104
|
+
|
|
97
105
|
const job = this.pending.shift();
|
|
106
|
+
|
|
98
107
|
if (job === undefined) {
|
|
99
108
|
return;
|
|
100
109
|
}
|
|
110
|
+
|
|
101
111
|
this.active = job;
|
|
102
112
|
this.started = this.now();
|
|
103
113
|
const epoch = this.epoch;
|
|
114
|
+
|
|
104
115
|
const alive = (): boolean => !this.disposed && epoch === this.epoch && !job.cancelled;
|
|
116
|
+
|
|
105
117
|
const isCurrent = (): boolean => alive() && job.valid?.() !== false;
|
|
118
|
+
|
|
106
119
|
void Promise.resolve()
|
|
107
120
|
.then(() => {
|
|
108
121
|
if (!isCurrent()) {
|
|
109
122
|
throw superseded();
|
|
110
123
|
}
|
|
124
|
+
|
|
111
125
|
this.client ??= this.options.createClient();
|
|
126
|
+
|
|
112
127
|
if (this.client === null) {
|
|
113
128
|
throw new EditorSearchError('unavailable', 'Regex search needs a search worker.');
|
|
114
129
|
}
|
|
130
|
+
|
|
115
131
|
const client = this.client;
|
|
132
|
+
|
|
116
133
|
const guarded: EditorSearchClient = {
|
|
117
134
|
search: (...args) => (isCurrent() ? client.search(...args) : Promise.reject(superseded())),
|
|
118
135
|
match: (...args) => (isCurrent() ? client.match(...args) : Promise.reject(superseded())),
|
|
@@ -122,6 +139,7 @@ export class FindSearchQueue {
|
|
|
122
139
|
release: () => client.release(),
|
|
123
140
|
dispose: () => client.dispose()
|
|
124
141
|
};
|
|
142
|
+
|
|
125
143
|
return job.run(guarded, isCurrent);
|
|
126
144
|
})
|
|
127
145
|
.then((value) => {
|
|
@@ -142,10 +160,12 @@ export class FindSearchQueue {
|
|
|
142
160
|
release(): void {
|
|
143
161
|
this.epoch++;
|
|
144
162
|
this.active?.reject(superseded());
|
|
163
|
+
|
|
145
164
|
for (const job of this.pending) {
|
|
146
165
|
job.cancelled = true;
|
|
147
166
|
job.reject(superseded());
|
|
148
167
|
}
|
|
168
|
+
|
|
149
169
|
this.pending = [];
|
|
150
170
|
this.cancelTimer?.();
|
|
151
171
|
this.cancelTimer = undefined;
|