@adecore/editor 0.21.1 → 0.22.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/code-block.js +26 -22
- package/dist/controller.d.ts +1 -0
- package/dist/controller.js +29 -43
- package/dist/engine.js +34 -42
- package/dist/fake.js +44 -97
- package/dist/keymap.d.ts +1 -2
- package/dist/keymap.js +18 -15
- package/dist/layout.d.ts +1 -1
- package/dist/layout.js +7 -6
- package/dist/listeners.d.ts +2 -2
- package/dist/listeners.js +2 -2
- package/dist/modifier-gesture.js +2 -2
- package/dist/native-input.d.ts +1 -0
- package/dist/native-input.js +5 -3
- package/dist/outline.js +2 -2
- package/dist/overview.js +0 -1
- package/dist/paint.js +11 -15
- package/dist/pointer.js +9 -2
- package/dist/testing.js +8 -5
- package/dist/tokens.d.ts +1 -0
- package/dist/tokens.js +9 -10
- package/dist/view.d.ts +1 -0
- package/dist/view.js +63 -92
- package/package.json +2 -2
- package/src/code-block.ts +27 -22
- package/src/controller.ts +32 -43
- package/src/engine.ts +37 -46
- package/src/fake.ts +44 -97
- package/src/keymap.ts +20 -17
- package/src/layout.ts +7 -6
- package/src/listeners.ts +3 -3
- package/src/modifier-gesture.ts +2 -2
- package/src/native-input.ts +6 -3
- package/src/outline.ts +2 -2
- package/src/overview.ts +0 -1
- package/src/paint.ts +12 -15
- package/src/pointer.ts +10 -2
- package/src/testing.ts +9 -5
- package/src/tokens.ts +10 -10
- package/src/types.ts +14 -8
- package/src/view.ts +73 -99
package/src/fake.ts
CHANGED
|
@@ -53,7 +53,7 @@ function positionIn(text: string, offset: number): EditorPosition {
|
|
|
53
53
|
return { line: lines.length - 1, character: lines.at(-1)!.length };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/* An editor without a DOM, for tests: `type`, `save` and `blur` do what a person would, the rest says what the
|
|
56
|
+
/* An editor without a DOM, for tests: `type`, `save` and `blur` do what a person would, the rest says what the host asked of it. */
|
|
57
57
|
export class FakeEditor implements Editor {
|
|
58
58
|
readonly element: HTMLElement;
|
|
59
59
|
readonly language: string | undefined;
|
|
@@ -67,10 +67,10 @@ export class FakeEditor implements Editor {
|
|
|
67
67
|
revealedLine: number | null;
|
|
68
68
|
focused = false;
|
|
69
69
|
indentation: EditorIndentation;
|
|
70
|
-
/* How often the
|
|
70
|
+
/* How often the host asked it to read the code face off the page again. */
|
|
71
71
|
fontRefreshes = 0;
|
|
72
72
|
disposed = false;
|
|
73
|
-
/* What the
|
|
73
|
+
/* What the host asked to find last; null once it ended the find. */
|
|
74
74
|
findQuery: EditorFindQuery | null = null;
|
|
75
75
|
findState: EditorFindState = { count: 0, current: null };
|
|
76
76
|
private text: string;
|
|
@@ -147,9 +147,7 @@ export class FakeEditor implements Editor {
|
|
|
147
147
|
source,
|
|
148
148
|
changes: [{ range: { start: positionIn(before, span.start), end: positionIn(before, span.end) }, text: span.text }]
|
|
149
149
|
};
|
|
150
|
-
|
|
151
|
-
listener(change);
|
|
152
|
-
}
|
|
150
|
+
emit(this.textChanges, change);
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
private readonly tracked = new Set<{ from: number; to: number; lost: boolean }>();
|
|
@@ -180,10 +178,7 @@ export class FakeEditor implements Editor {
|
|
|
180
178
|
}
|
|
181
179
|
|
|
182
180
|
onTextChange(listener: (change: EditorTextChange) => void): () => void {
|
|
183
|
-
this.textChanges
|
|
184
|
-
return () => {
|
|
185
|
-
this.textChanges.delete(listener);
|
|
186
|
-
};
|
|
181
|
+
return subscribe(this.textChanges, listener);
|
|
187
182
|
}
|
|
188
183
|
|
|
189
184
|
positionAt(offset: number): EditorPosition {
|
|
@@ -216,7 +211,7 @@ export class FakeEditor implements Editor {
|
|
|
216
211
|
return subscribe(this.blurs, listener);
|
|
217
212
|
}
|
|
218
213
|
|
|
219
|
-
/* How the
|
|
214
|
+
/* How the host last asked the view to follow a jump. */
|
|
220
215
|
lastReveal: EditorReveal | null = null;
|
|
221
216
|
|
|
222
217
|
revealLine(line: number, reveal: EditorReveal = 'center'): void {
|
|
@@ -224,7 +219,7 @@ export class FakeEditor implements Editor {
|
|
|
224
219
|
this.lastReveal = reveal;
|
|
225
220
|
}
|
|
226
221
|
|
|
227
|
-
/* Counts plain occurrences only; the matcher itself is the editor's, and a test here is about the
|
|
222
|
+
/* Counts plain occurrences only; the matcher itself is the editor's, and a test here is about the host's side. */
|
|
228
223
|
find(query: EditorFindQuery | null): void {
|
|
229
224
|
this.findQuery = query;
|
|
230
225
|
const needle = query === null || query.text === '' ? '' : query.caseSensitive ? query.text : query.text.toLowerCase();
|
|
@@ -233,12 +228,12 @@ export class FakeEditor implements Editor {
|
|
|
233
228
|
this.announceFind({ count, current: count === 0 ? null : 0 });
|
|
234
229
|
}
|
|
235
230
|
|
|
236
|
-
/* What the
|
|
231
|
+
/* What the host asked to replace, one match at a time and all at once. */
|
|
237
232
|
replacements: string[] = [];
|
|
238
233
|
replaceOptions: EditorReplaceOptions[] = [];
|
|
239
|
-
/* What the
|
|
234
|
+
/* What the host last asked to show under the match. */
|
|
240
235
|
replacePreview: string | null = null;
|
|
241
|
-
/* What the
|
|
236
|
+
/* What the host asked to select all matches of, and how many it was told there were. */
|
|
242
237
|
selectedMatches = 0;
|
|
243
238
|
lastFindFromCursor: { query: EditorFindQuery; direction: 1 | -1 } | null = null;
|
|
244
239
|
replacementsOfAll: string[] = [];
|
|
@@ -278,10 +273,7 @@ export class FakeEditor implements Editor {
|
|
|
278
273
|
}
|
|
279
274
|
|
|
280
275
|
onFind(listener: (state: EditorFindState) => void): () => void {
|
|
281
|
-
this.finds
|
|
282
|
-
return () => {
|
|
283
|
-
this.finds.delete(listener);
|
|
284
|
-
};
|
|
276
|
+
return subscribe(this.finds, listener);
|
|
285
277
|
}
|
|
286
278
|
|
|
287
279
|
endFind(): void {
|
|
@@ -292,39 +284,39 @@ export class FakeEditor implements Editor {
|
|
|
292
284
|
this.wrap = wrap;
|
|
293
285
|
}
|
|
294
286
|
|
|
295
|
-
/* What the
|
|
287
|
+
/* What the host handed it last; null while it reads the structure itself. */
|
|
296
288
|
blocks: readonly EditorBlock[] | null = null;
|
|
297
289
|
private readonly scopes = new Set<(scope: readonly EditorBlock[]) => void>();
|
|
298
290
|
|
|
299
|
-
/* What the
|
|
291
|
+
/* What the host marked as changed last. */
|
|
300
292
|
changeMarks: readonly EditorChangeMark[] = [];
|
|
301
293
|
|
|
302
294
|
setChangeMarks(marks: readonly EditorChangeMark[]): void {
|
|
303
295
|
this.changeMarks = marks;
|
|
304
296
|
}
|
|
305
297
|
|
|
306
|
-
/* The tinted lines the
|
|
298
|
+
/* The tinted lines the host set last. */
|
|
307
299
|
lineHighlights: readonly EditorLineHighlight[] = [];
|
|
308
300
|
|
|
309
301
|
setLineHighlights(highlights: readonly EditorLineHighlight[]): void {
|
|
310
302
|
this.lineHighlights = highlights;
|
|
311
303
|
}
|
|
312
304
|
|
|
313
|
-
/* What the
|
|
305
|
+
/* What the host drew as code outside the document: the text and how, and into which element. */
|
|
314
306
|
codeBlocks: { container: HTMLElement; text: string; options: EditorCodeBlockOptions }[] = [];
|
|
315
307
|
|
|
316
308
|
renderCode(container: HTMLElement, text: string, options: EditorCodeBlockOptions = {}): void {
|
|
317
309
|
this.codeBlocks.push({ container, text, options });
|
|
318
310
|
}
|
|
319
311
|
|
|
320
|
-
/* The cursors of agents the
|
|
312
|
+
/* The cursors of agents the host set last. */
|
|
321
313
|
remoteCursors: readonly EditorRemoteCursor[] = [];
|
|
322
314
|
|
|
323
315
|
setRemoteCursors(cursors: readonly EditorRemoteCursor[]): void {
|
|
324
316
|
this.remoteCursors = cursors;
|
|
325
317
|
}
|
|
326
318
|
|
|
327
|
-
/* What the
|
|
319
|
+
/* What the host marked as written by an agent last. */
|
|
328
320
|
attributionMarks: readonly EditorAttributionMark[] = [];
|
|
329
321
|
private readonly attributionHovers = new Set<(hover: EditorAttributionHover | null) => void>();
|
|
330
322
|
|
|
@@ -333,10 +325,7 @@ export class FakeEditor implements Editor {
|
|
|
333
325
|
}
|
|
334
326
|
|
|
335
327
|
onAttributionHover(listener: (hover: EditorAttributionHover | null) => void): () => void {
|
|
336
|
-
this.attributionHovers
|
|
337
|
-
return () => {
|
|
338
|
-
this.attributionHovers.delete(listener);
|
|
339
|
-
};
|
|
328
|
+
return subscribe(this.attributionHovers, listener);
|
|
340
329
|
}
|
|
341
330
|
|
|
342
331
|
/* The pointer on the bar of a mark, or leaving it. The rect is where `rectAt` puts the mark's first line unless given. */
|
|
@@ -346,9 +335,7 @@ export class FakeEditor implements Editor {
|
|
|
346
335
|
mark === undefined
|
|
347
336
|
? null
|
|
348
337
|
: { id: mark.id, rect: rect ?? this.rectAt({ line: mark.startLine - 1, character: 0 }) ?? { left: 0, top: 0, right: 0, bottom: 0 } };
|
|
349
|
-
|
|
350
|
-
listener(hover);
|
|
351
|
-
}
|
|
338
|
+
emit(this.attributionHovers, hover);
|
|
352
339
|
}
|
|
353
340
|
|
|
354
341
|
/* What each owner put next to lines of the text last. */
|
|
@@ -388,14 +375,14 @@ export class FakeEditor implements Editor {
|
|
|
388
375
|
}
|
|
389
376
|
}
|
|
390
377
|
|
|
391
|
-
/* The code vision rows the
|
|
378
|
+
/* The code vision rows the host set last. */
|
|
392
379
|
codeVision: readonly EditorCodeVision[] = [];
|
|
393
380
|
|
|
394
381
|
setCodeVision(rows: readonly EditorCodeVision[]): void {
|
|
395
382
|
this.codeVision = rows;
|
|
396
383
|
}
|
|
397
384
|
|
|
398
|
-
/* The button the
|
|
385
|
+
/* The button the host set in the gutter last. */
|
|
399
386
|
gutterAction: EditorGutterAction | null = null;
|
|
400
387
|
private readonly gutterActionListeners = new Set<(line: number) => void>();
|
|
401
388
|
|
|
@@ -404,10 +391,7 @@ export class FakeEditor implements Editor {
|
|
|
404
391
|
}
|
|
405
392
|
|
|
406
393
|
onGutterAction(listener: (line: number) => void): () => void {
|
|
407
|
-
this.gutterActionListeners
|
|
408
|
-
return () => {
|
|
409
|
-
this.gutterActionListeners.delete(listener);
|
|
410
|
-
};
|
|
394
|
+
return subscribe(this.gutterActionListeners, listener);
|
|
411
395
|
}
|
|
412
396
|
|
|
413
397
|
/* What each owner put in the gutter last. */
|
|
@@ -423,25 +407,18 @@ export class FakeEditor implements Editor {
|
|
|
423
407
|
}
|
|
424
408
|
|
|
425
409
|
onGutterMarker(listener: (id: string) => void): () => void {
|
|
426
|
-
this.gutterMarkerListeners
|
|
427
|
-
return () => {
|
|
428
|
-
this.gutterMarkerListeners.delete(listener);
|
|
429
|
-
};
|
|
410
|
+
return subscribe(this.gutterMarkerListeners, listener);
|
|
430
411
|
}
|
|
431
412
|
|
|
432
413
|
/* A press on a gutter marker. */
|
|
433
414
|
pressGutterMarker(id: string): void {
|
|
434
|
-
|
|
435
|
-
listener(id);
|
|
436
|
-
}
|
|
415
|
+
emit(this.gutterMarkerListeners, id);
|
|
437
416
|
}
|
|
438
417
|
|
|
439
418
|
/* A press on the gutter button, if there is one. */
|
|
440
419
|
pressGutterAction(): void {
|
|
441
420
|
if (this.gutterAction !== null) {
|
|
442
|
-
|
|
443
|
-
listener(this.gutterAction.line);
|
|
444
|
-
}
|
|
421
|
+
emit(this.gutterActionListeners, this.gutterAction.line);
|
|
445
422
|
}
|
|
446
423
|
}
|
|
447
424
|
|
|
@@ -450,17 +427,12 @@ export class FakeEditor implements Editor {
|
|
|
450
427
|
}
|
|
451
428
|
|
|
452
429
|
onScope(listener: (scope: readonly EditorBlock[]) => void): () => void {
|
|
453
|
-
this.scopes
|
|
454
|
-
return () => {
|
|
455
|
-
this.scopes.delete(listener);
|
|
456
|
-
};
|
|
430
|
+
return subscribe(this.scopes, listener);
|
|
457
431
|
}
|
|
458
432
|
|
|
459
|
-
/* The caret moving into blocks, for a test of what the
|
|
433
|
+
/* The caret moving into blocks, for a test of what the host draws from it. */
|
|
460
434
|
enterScope(scope: readonly EditorBlock[]): void {
|
|
461
|
-
|
|
462
|
-
listener(scope);
|
|
463
|
-
}
|
|
435
|
+
emit(this.scopes, scope);
|
|
464
436
|
}
|
|
465
437
|
|
|
466
438
|
smartKeys: Partial<EditorSmartKeys>;
|
|
@@ -506,7 +478,7 @@ export class FakeEditor implements Editor {
|
|
|
506
478
|
this.indentation = indentation;
|
|
507
479
|
}
|
|
508
480
|
|
|
509
|
-
/* What the
|
|
481
|
+
/* What the host marked and highlighted last. */
|
|
510
482
|
markers: readonly EditorMarker[] = [];
|
|
511
483
|
highlights: readonly EditorHighlight[] = [];
|
|
512
484
|
caret: EditorPosition = { line: 0, character: 0 };
|
|
@@ -522,7 +494,7 @@ export class FakeEditor implements Editor {
|
|
|
522
494
|
this.markers = markers;
|
|
523
495
|
}
|
|
524
496
|
|
|
525
|
-
/* What the
|
|
497
|
+
/* What the host drew as hints last. */
|
|
526
498
|
inlayHints: readonly EditorInlayHint[] = [];
|
|
527
499
|
|
|
528
500
|
setInlayHints(hints: readonly EditorInlayHint[]): void {
|
|
@@ -536,14 +508,14 @@ export class FakeEditor implements Editor {
|
|
|
536
508
|
this.ghost = ghost;
|
|
537
509
|
}
|
|
538
510
|
|
|
539
|
-
/* What the
|
|
511
|
+
/* What the host classified last. */
|
|
540
512
|
semanticTokens: readonly EditorSemanticToken[] | null = null;
|
|
541
513
|
|
|
542
514
|
setSemanticTokens(tokens: readonly EditorSemanticToken[] | null): void {
|
|
543
515
|
this.semanticTokens = tokens;
|
|
544
516
|
}
|
|
545
517
|
|
|
546
|
-
/* What the
|
|
518
|
+
/* What the host drew as a link last. */
|
|
547
519
|
link: EditorRange | null = null;
|
|
548
520
|
|
|
549
521
|
setLink(range: EditorRange | null): void {
|
|
@@ -558,7 +530,7 @@ export class FakeEditor implements Editor {
|
|
|
558
530
|
return this.caret;
|
|
559
531
|
}
|
|
560
532
|
|
|
561
|
-
/* The folds the
|
|
533
|
+
/* The folds the host asked to keep. */
|
|
562
534
|
folds: EditorFolds = { collapsed: [], custom: [] };
|
|
563
535
|
|
|
564
536
|
getFolds(): EditorFolds {
|
|
@@ -616,40 +588,28 @@ export class FakeEditor implements Editor {
|
|
|
616
588
|
this.lastReveal = reveal;
|
|
617
589
|
this.caret = range.end;
|
|
618
590
|
this.selection = range;
|
|
619
|
-
|
|
620
|
-
listener(range.end);
|
|
621
|
-
}
|
|
591
|
+
emit(this.carets, range.end);
|
|
622
592
|
}
|
|
623
593
|
|
|
624
594
|
/* The caret a person's click or key moved. */
|
|
625
595
|
moveCaret(position: EditorPosition): void {
|
|
626
596
|
this.caret = position;
|
|
627
597
|
this.selection = null;
|
|
628
|
-
|
|
629
|
-
listener(position);
|
|
630
|
-
}
|
|
598
|
+
emit(this.carets, position);
|
|
631
599
|
}
|
|
632
600
|
|
|
633
601
|
onCaret(listener: (position: EditorPosition) => void): () => void {
|
|
634
|
-
this.carets
|
|
635
|
-
return () => {
|
|
636
|
-
this.carets.delete(listener);
|
|
637
|
-
};
|
|
602
|
+
return subscribe(this.carets, listener);
|
|
638
603
|
}
|
|
639
604
|
|
|
640
605
|
onHover(listener: (hover: EditorHover | null) => void): () => void {
|
|
641
|
-
this.hovers
|
|
642
|
-
return () => {
|
|
643
|
-
this.hovers.delete(listener);
|
|
644
|
-
};
|
|
606
|
+
return subscribe(this.hovers, listener);
|
|
645
607
|
}
|
|
646
608
|
|
|
647
609
|
/* The pointer resting on a position, or leaving the text. */
|
|
648
610
|
hover(position: EditorPosition | null): void {
|
|
649
611
|
const hover = position === null ? null : { position, rect: this.rectAt(position)! };
|
|
650
|
-
|
|
651
|
-
listener(hover);
|
|
652
|
-
}
|
|
612
|
+
emit(this.hovers, hover);
|
|
653
613
|
}
|
|
654
614
|
|
|
655
615
|
rectAt(position: EditorPosition): EditorRect | null {
|
|
@@ -685,27 +645,18 @@ export class FakeEditor implements Editor {
|
|
|
685
645
|
}
|
|
686
646
|
|
|
687
647
|
onKeyDown(handler: EditorKeyHandler): () => void {
|
|
688
|
-
this.keys
|
|
689
|
-
return () => {
|
|
690
|
-
this.keys.delete(handler);
|
|
691
|
-
};
|
|
648
|
+
return subscribe(this.keys, handler);
|
|
692
649
|
}
|
|
693
650
|
|
|
694
651
|
private readonly clickHandlers = new Set<EditorClickHandler>();
|
|
695
652
|
private readonly contextListeners = new Set<(menu: EditorContextMenu) => void>();
|
|
696
653
|
|
|
697
654
|
onClick(handler: EditorClickHandler): () => void {
|
|
698
|
-
this.clickHandlers
|
|
699
|
-
return () => {
|
|
700
|
-
this.clickHandlers.delete(handler);
|
|
701
|
-
};
|
|
655
|
+
return subscribe(this.clickHandlers, handler);
|
|
702
656
|
}
|
|
703
657
|
|
|
704
658
|
onContextMenu(listener: (menu: EditorContextMenu) => void): () => void {
|
|
705
|
-
this.contextListeners
|
|
706
|
-
return () => {
|
|
707
|
-
this.contextListeners.delete(listener);
|
|
708
|
-
};
|
|
659
|
+
return subscribe(this.contextListeners, listener);
|
|
709
660
|
}
|
|
710
661
|
|
|
711
662
|
/* A press on a character as the editor sees it first; true when a handler took it, and the caret stays where it is otherwise only until the test moves it. */
|
|
@@ -715,9 +666,7 @@ export class FakeEditor implements Editor {
|
|
|
715
666
|
|
|
716
667
|
/* The context menu asked for at a position. */
|
|
717
668
|
openContextMenu(menu: EditorContextMenu): void {
|
|
718
|
-
|
|
719
|
-
listener(menu);
|
|
720
|
-
}
|
|
669
|
+
emit(this.contextListeners, menu);
|
|
721
670
|
}
|
|
722
671
|
|
|
723
672
|
/* A key as the editor sees it first; true when a handler took it. */
|
|
@@ -805,12 +754,10 @@ export class FakeEditor implements Editor {
|
|
|
805
754
|
|
|
806
755
|
private announceFind(state: EditorFindState): void {
|
|
807
756
|
this.findState = state;
|
|
808
|
-
|
|
809
|
-
listener(state);
|
|
810
|
-
}
|
|
757
|
+
emit(this.finds, state);
|
|
811
758
|
}
|
|
812
759
|
|
|
813
|
-
/* A test that drives an editor its
|
|
760
|
+
/* A test that drives an editor its host already disposed has found a leak. */
|
|
814
761
|
private assertLive(): void {
|
|
815
762
|
if (this.disposed) {
|
|
816
763
|
throw new Error('The editor is disposed');
|
package/src/keymap.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { EditorCommand } from '@adecore/editor-core';
|
|
2
|
-
import type { EditorViewCommand } from './types.ts';
|
|
3
2
|
import { chordOf, KEYMAP, KEYMAP_IDS, type Keymap, type KeymapId, parseChord } from './keymap-table.ts';
|
|
4
|
-
import type { KeyChord } from './types.ts';
|
|
3
|
+
import type { EditorViewCommand, KeyChord } from './types.ts';
|
|
5
4
|
|
|
6
5
|
export interface KeyLike {
|
|
7
6
|
key: string;
|
|
@@ -30,6 +29,10 @@ function command(name: EditorCommand): KeyAction {
|
|
|
30
29
|
return { type: 'command', command: name };
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
function view(name: EditorViewCommand): KeyAction {
|
|
33
|
+
return { type: 'view', command: name };
|
|
34
|
+
}
|
|
35
|
+
|
|
33
36
|
/* What each key of the table does in the editor. A key of the table that is not here is a language command, which the host answers. */
|
|
34
37
|
const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
|
|
35
38
|
undo: command('undo'),
|
|
@@ -69,21 +72,21 @@ const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
|
|
|
69
72
|
selectTextStart: { type: 'edge', end: false, extend: true },
|
|
70
73
|
textEnd: { type: 'edge', end: true, extend: false },
|
|
71
74
|
selectTextEnd: { type: 'edge', end: true, extend: true },
|
|
72
|
-
collapse:
|
|
73
|
-
expand:
|
|
74
|
-
collapseAll:
|
|
75
|
-
expandAll:
|
|
76
|
-
collapseRecursively:
|
|
77
|
-
expandRecursively:
|
|
78
|
-
foldSelection:
|
|
79
|
-
collapseDocComments:
|
|
80
|
-
expandDocComments:
|
|
81
|
-
expandAllToLevel1:
|
|
82
|
-
expandAllToLevel2:
|
|
83
|
-
expandAllToLevel3:
|
|
84
|
-
expandAllToLevel4:
|
|
85
|
-
expandAllToLevel5:
|
|
86
|
-
toggleColumnMode:
|
|
75
|
+
collapse: view('collapseRegion'),
|
|
76
|
+
expand: view('expandRegion'),
|
|
77
|
+
collapseAll: view('collapseAllRegions'),
|
|
78
|
+
expandAll: view('expandAllRegions'),
|
|
79
|
+
collapseRecursively: view('collapseRegionRecursively'),
|
|
80
|
+
expandRecursively: view('expandRegionRecursively'),
|
|
81
|
+
foldSelection: view('foldSelection'),
|
|
82
|
+
collapseDocComments: view('collapseDocComments'),
|
|
83
|
+
expandDocComments: view('expandDocComments'),
|
|
84
|
+
expandAllToLevel1: view('expandAllToLevel1'),
|
|
85
|
+
expandAllToLevel2: view('expandAllToLevel2'),
|
|
86
|
+
expandAllToLevel3: view('expandAllToLevel3'),
|
|
87
|
+
expandAllToLevel4: view('expandAllToLevel4'),
|
|
88
|
+
expandAllToLevel5: view('expandAllToLevel5'),
|
|
89
|
+
toggleColumnMode: view('toggleColumnMode')
|
|
87
90
|
};
|
|
88
91
|
|
|
89
92
|
const PUNCTUATION_CODES: Readonly<Record<string, string>> = {
|
package/src/layout.ts
CHANGED
|
@@ -136,6 +136,7 @@ const DEFAULT_BLOCK_HEIGHT = 40;
|
|
|
136
136
|
const WRAP_INDENT_STEP = 2;
|
|
137
137
|
const GEOMETRY_CACHE_SIZE = 400;
|
|
138
138
|
const PLAIN = /^[\x20-\x7e\t]*$/;
|
|
139
|
+
const GRAPHEME_LIMIT = 20000;
|
|
139
140
|
const SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
140
141
|
|
|
141
142
|
function lowerBound(values: readonly number[], value: number): number {
|
|
@@ -152,10 +153,10 @@ function lowerBound(values: readonly number[], value: number): number {
|
|
|
152
153
|
return low;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
/* Where each character starts, with the length last. A
|
|
156
|
+
/* Where each character starts, with the length last. A very long line is cut by code unit, since splitting it by grapheme would cost more than it shows. */
|
|
156
157
|
function characterOffsets(text: string): number[] {
|
|
157
158
|
const offsets: number[] = [];
|
|
158
|
-
if (text.length >
|
|
159
|
+
if (text.length > GRAPHEME_LIMIT || PLAIN.test(text)) {
|
|
159
160
|
for (let i = 0; i < text.length; i++) {
|
|
160
161
|
offsets.push(i);
|
|
161
162
|
}
|
|
@@ -432,8 +433,8 @@ export class EditorLayout {
|
|
|
432
433
|
}
|
|
433
434
|
const fold = folds.get(line);
|
|
434
435
|
const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
|
|
435
|
-
const tail = fold?.closer !== undefined && last > line ? this.
|
|
436
|
-
const head = fold?.head !== undefined && last > line ? this.
|
|
436
|
+
const tail = fold?.closer !== undefined && last > line ? this.clampToLine(fold.closer, last) : null;
|
|
437
|
+
const head = fold?.head !== undefined && last > line ? this.clampToLine(fold.head, line) : null;
|
|
437
438
|
const index = this.rows.length;
|
|
438
439
|
const bounds = this.wrapWidth === null ? null : this.document.getLine(line);
|
|
439
440
|
const subRows = this.rowCountOf(line, bounds === null ? 0 : bounds.end - bounds.start);
|
|
@@ -461,9 +462,9 @@ export class EditorLayout {
|
|
|
461
462
|
}
|
|
462
463
|
|
|
463
464
|
/* An offset of a fold clamped to its line, since the fold may be a moment older than an edit. */
|
|
464
|
-
private
|
|
465
|
+
private clampToLine(offset: number, line: number): number {
|
|
465
466
|
const bounds = this.document.getLine(line);
|
|
466
|
-
return Math.min(bounds.end, Math.max(bounds.start,
|
|
467
|
+
return Math.min(bounds.end, Math.max(bounds.start, offset));
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
private reflow(): void {
|
package/src/listeners.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type Listener = () => void;
|
|
2
2
|
|
|
3
|
-
export function subscribe(listeners: Set<
|
|
3
|
+
export function subscribe<T>(listeners: Set<T>, listener: T): () => void {
|
|
4
4
|
listeners.add(listener);
|
|
5
5
|
return () => {
|
|
6
6
|
listeners.delete(listener);
|
|
@@ -8,8 +8,8 @@ export function subscribe(listeners: Set<Listener>, listener: Listener): () => v
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/* Over a copy, so a listener that unsubscribes itself does not skip the next one. */
|
|
11
|
-
export function emit(listeners: Set<
|
|
11
|
+
export function emit<Args extends unknown[]>(listeners: Set<(...args: Args) => void>, ...args: Args): void {
|
|
12
12
|
for (const listener of [...listeners]) {
|
|
13
|
-
listener();
|
|
13
|
+
listener(...args);
|
|
14
14
|
}
|
|
15
15
|
}
|
package/src/modifier-gesture.ts
CHANGED
|
@@ -45,8 +45,8 @@ export class DoubleModifierGesture {
|
|
|
45
45
|
|
|
46
46
|
/* Another modifier held with the one that counts spoils the gesture. */
|
|
47
47
|
private spoiled(event: GestureEvent): boolean {
|
|
48
|
-
const
|
|
49
|
-
return
|
|
48
|
+
const other = this.modifier === 'Alt' ? event.ctrlKey : event.altKey;
|
|
49
|
+
return other || event.metaKey || event.shiftKey;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
keydown(event: GestureEvent): GestureAction | null {
|
package/src/native-input.ts
CHANGED
|
@@ -119,8 +119,7 @@ export class NativeInput {
|
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
121
|
if (this.model.getSelections().length > 1) {
|
|
122
|
-
|
|
123
|
-
this.host.replace(this.element.value.slice(context.start, Math.max(context.start, this.element.value.length - suffix)));
|
|
122
|
+
this.host.replace(this.replacedText(context));
|
|
124
123
|
return;
|
|
125
124
|
}
|
|
126
125
|
const edit = {
|
|
@@ -172,7 +171,11 @@ export class NativeInput {
|
|
|
172
171
|
}
|
|
173
172
|
|
|
174
173
|
private composedText(): string {
|
|
175
|
-
|
|
174
|
+
return this.replacedText(this.compositionContext!);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* What the textarea now holds where the selection of `context` was, the text after it kept. */
|
|
178
|
+
private replacedText(context: InputContext): string {
|
|
176
179
|
const suffix = context.value.length - context.end;
|
|
177
180
|
return this.element.value.slice(context.start, Math.max(context.start, this.element.value.length - suffix));
|
|
178
181
|
}
|
package/src/outline.ts
CHANGED
|
@@ -120,7 +120,7 @@ export function structuralEntries(
|
|
|
120
120
|
return entries;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
function
|
|
123
|
+
function toBlock(entry: OutlineEntry, document: OutlineDocument): OutlineBlock {
|
|
124
124
|
const startLine = document.positionAt(entry.from).line;
|
|
125
125
|
return {
|
|
126
126
|
startLine,
|
|
@@ -187,7 +187,7 @@ export class Outline {
|
|
|
187
187
|
blocks(document: OutlineDocument): readonly OutlineBlock[] {
|
|
188
188
|
if (this.cache?.version !== this.version) {
|
|
189
189
|
const sorted = (this.provided ?? this.structure)
|
|
190
|
-
.map((entry) =>
|
|
190
|
+
.map((entry) => toBlock(entry, document))
|
|
191
191
|
.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
192
192
|
this.cache = { version: this.version, blocks: sorted.filter((block, index) => index === 0 || block.startLine !== sorted[index - 1]!.startLine) };
|
|
193
193
|
}
|
package/src/overview.ts
CHANGED
|
@@ -56,7 +56,6 @@ export function overviewTicks(spans: readonly OverviewSpan[], contentHeight: num
|
|
|
56
56
|
/* What is painted later is on top: the current find match, and the worse a problem is the later it goes. */
|
|
57
57
|
const PAINT_ORDER: Partial<Record<OverviewKind, number>> = { warning: 1, error: 2, 'find-current': 3 };
|
|
58
58
|
|
|
59
|
-
/* Draws the ticks. */
|
|
60
59
|
export function paintOverview(container: HTMLElement, ticks: readonly OverviewTick[]): void {
|
|
61
60
|
const document = container.ownerDocument;
|
|
62
61
|
const order = (tick: OverviewTick): number => PAINT_ORDER[tick.kind] ?? 0;
|
package/src/paint.ts
CHANGED
|
@@ -345,6 +345,15 @@ export interface GutterPaint {
|
|
|
345
345
|
markers: ReadonlyMap<number, { id: string; label: string }>;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
function gutterButton(document: Document, className: string, label: string): HTMLButtonElement {
|
|
349
|
+
const button = document.createElement('button');
|
|
350
|
+
button.type = 'button';
|
|
351
|
+
button.className = className;
|
|
352
|
+
button.tabIndex = -1;
|
|
353
|
+
button.setAttribute('aria-label', label);
|
|
354
|
+
return button;
|
|
355
|
+
}
|
|
356
|
+
|
|
348
357
|
/* The line numbers of the visible rows, and the fold control of the lines that have one. */
|
|
349
358
|
export function paintGutter(container: HTMLElement, layout: EditorLayout, rows: readonly LayoutRow[], paint: GutterPaint): void {
|
|
350
359
|
const document = container.ownerDocument;
|
|
@@ -365,22 +374,14 @@ export function paintGutter(container: HTMLElement, layout: EditorLayout, rows:
|
|
|
365
374
|
number.textContent = String(row.line + 1);
|
|
366
375
|
item.append(number);
|
|
367
376
|
if (paint.action?.line === row.line) {
|
|
368
|
-
const button = document
|
|
369
|
-
button.type = 'button';
|
|
370
|
-
button.className = 'se-gutter-action';
|
|
377
|
+
const button = gutterButton(document, 'se-gutter-action', paint.action.label);
|
|
371
378
|
button.dataset.gutterAction = String(row.line);
|
|
372
|
-
button.tabIndex = -1;
|
|
373
|
-
button.setAttribute('aria-label', paint.action.label);
|
|
374
379
|
item.append(button);
|
|
375
380
|
}
|
|
376
381
|
const marker = paint.markers.get(row.line);
|
|
377
382
|
if (marker !== undefined) {
|
|
378
|
-
const button = document
|
|
379
|
-
button.type = 'button';
|
|
380
|
-
button.className = 'se-gutter-marker';
|
|
383
|
+
const button = gutterButton(document, 'se-gutter-marker', marker.label);
|
|
381
384
|
button.dataset.gutterMarker = marker.id;
|
|
382
|
-
button.tabIndex = -1;
|
|
383
|
-
button.setAttribute('aria-label', marker.label);
|
|
384
385
|
item.append(button);
|
|
385
386
|
}
|
|
386
387
|
const sign = paint.highlights.get(row.line)?.sign;
|
|
@@ -412,12 +413,8 @@ export function paintGutter(container: HTMLElement, layout: EditorLayout, rows:
|
|
|
412
413
|
}
|
|
413
414
|
const collapsed = paint.foldable.get(row.line);
|
|
414
415
|
if (collapsed !== undefined) {
|
|
415
|
-
const button = document
|
|
416
|
-
button.type = 'button';
|
|
417
|
-
button.className = collapsed ? 'se-fold-toggle se-folded' : 'se-fold-toggle';
|
|
416
|
+
const button = gutterButton(document, collapsed ? 'se-fold-toggle se-folded' : 'se-fold-toggle', collapsed ? 'Expand' : 'Collapse');
|
|
418
417
|
button.dataset.foldLine = String(row.line);
|
|
419
|
-
button.tabIndex = -1;
|
|
420
|
-
button.setAttribute('aria-label', collapsed ? 'Expand' : 'Collapse');
|
|
421
418
|
item.append(button);
|
|
422
419
|
}
|
|
423
420
|
fragment.append(item);
|
package/src/pointer.ts
CHANGED
|
@@ -77,6 +77,14 @@ const MULTI_CLICK_DISTANCE = 4;
|
|
|
77
77
|
/* How far the pointer goes, in pixels, before a press on a selection starts carrying it. */
|
|
78
78
|
const CARRY_THRESHOLD = 5;
|
|
79
79
|
|
|
80
|
+
/* How far `value` lies outside `[low, high]`, negative below it and zero inside. */
|
|
81
|
+
function overshoot(value: number, low: number, high: number): number {
|
|
82
|
+
if (value < low) {
|
|
83
|
+
return value - low;
|
|
84
|
+
}
|
|
85
|
+
return value > high ? value - high : 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
/* The selection a mouse makes: a click, a drag, shift to extend, alt to add a caret or, dragged, a column, a double click for a word and a triple for a line, which a drag then grows by words and lines. */
|
|
81
89
|
export class PointerSelection {
|
|
82
90
|
private drag: Drag | undefined;
|
|
@@ -313,8 +321,8 @@ export class PointerSelection {
|
|
|
313
321
|
return;
|
|
314
322
|
}
|
|
315
323
|
const rect = viewport.getBoundingClientRect();
|
|
316
|
-
const dy = this.drag.y
|
|
317
|
-
const dx = this.drag.x
|
|
324
|
+
const dy = overshoot(this.drag.y, rect.top, rect.bottom);
|
|
325
|
+
const dx = overshoot(this.drag.x, rect.left, rect.right);
|
|
318
326
|
if (dy || dx) {
|
|
319
327
|
viewport.scrollTop += Math.max(-SCROLL_STEP, Math.min(SCROLL_STEP, dy));
|
|
320
328
|
viewport.scrollLeft += Math.max(-SCROLL_STEP, Math.min(SCROLL_STEP, dx));
|