@adecore/editor 0.41.1 → 0.43.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 +13 -2
- package/dist/controller.js +110 -9
- package/dist/editor.css +19 -0
- package/dist/engine.js +82 -5
- package/dist/fake.d.ts +31 -2
- package/dist/fake.js +100 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/keymap-table.d.ts +28 -1
- package/dist/keymap-table.js +34 -3
- package/dist/keymap.js +23 -1
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +6 -1
- package/dist/outline.d.ts +1 -0
- package/dist/outline.js +10 -0
- package/dist/overview.d.ts +1 -1
- package/dist/paint.js +12 -2
- package/dist/todos.d.ts +23 -0
- package/dist/todos.js +86 -0
- package/dist/types.d.ts +34 -3
- package/dist/view.d.ts +17 -0
- package/dist/view.js +113 -12
- package/dist/zoom.d.ts +18 -0
- package/dist/zoom.js +41 -0
- package/package.json +2 -2
- package/src/controller.ts +150 -11
- package/src/editor.css +19 -0
- package/src/engine.ts +103 -5
- package/src/fake.ts +132 -3
- package/src/index.ts +7 -0
- package/src/keymap-table.ts +63 -4
- package/src/keymap.ts +23 -1
- package/src/layout.ts +9 -1
- package/src/outline.ts +13 -0
- package/src/overview.ts +1 -1
- package/src/paint.ts +11 -2
- package/src/todos.ts +111 -0
- package/src/types.ts +76 -4
- package/src/view.ts +143 -13
- package/src/zoom.ts +62 -0
package/src/controller.ts
CHANGED
|
@@ -8,13 +8,16 @@ import { NativeInput } from './native-input.ts';
|
|
|
8
8
|
import { columnSelections, sameCell } from './column-selection.ts';
|
|
9
9
|
import { PointerSelection } from './pointer.ts';
|
|
10
10
|
import { TouchSelection } from './touch.ts';
|
|
11
|
+
import { WheelZoom } from './zoom.ts';
|
|
11
12
|
import type {
|
|
12
13
|
EditorClick,
|
|
13
14
|
EditorClickHandler,
|
|
14
15
|
EditorInlayClick,
|
|
15
16
|
EditorInlayClickHandler,
|
|
16
17
|
EditorContextMenu,
|
|
18
|
+
EditorCopy,
|
|
17
19
|
EditorKeyHandler,
|
|
20
|
+
EditorPasteOptions,
|
|
18
21
|
EditorPosition,
|
|
19
22
|
EditorRunCommand,
|
|
20
23
|
EditorSelectionRanges,
|
|
@@ -29,6 +32,10 @@ export interface ControllerOptions {
|
|
|
29
32
|
apple: boolean;
|
|
30
33
|
save(): void;
|
|
31
34
|
blur(): void;
|
|
35
|
+
/* One pixel larger (1) or smaller (-1), or back to the page's size (0). */
|
|
36
|
+
zoom(step: -1 | 0 | 1): void;
|
|
37
|
+
/* Whether the zoom modifier with the wheel, or a pinch, zooms. */
|
|
38
|
+
wheelZooms(): boolean;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
const MODIFYING = new Set<EditorCommand>([
|
|
@@ -40,9 +47,12 @@ const MODIFYING = new Set<EditorCommand>([
|
|
|
40
47
|
'deleteLine',
|
|
41
48
|
'moveLineUp',
|
|
42
49
|
'moveLineDown',
|
|
50
|
+
'moveStatementUp',
|
|
51
|
+
'moveStatementDown',
|
|
43
52
|
'toggleLineComment',
|
|
44
53
|
'toggleBlockComment',
|
|
45
54
|
'startNewLine',
|
|
55
|
+
'completeCurrentStatement',
|
|
46
56
|
'startNewLineBefore',
|
|
47
57
|
'splitLine',
|
|
48
58
|
'joinLines',
|
|
@@ -53,9 +63,18 @@ const MODIFYING = new Set<EditorCommand>([
|
|
|
53
63
|
'outdent',
|
|
54
64
|
'insertNewline',
|
|
55
65
|
'smartBackspace',
|
|
56
|
-
'deleteForward'
|
|
66
|
+
'deleteForward',
|
|
67
|
+
'sortLines',
|
|
68
|
+
'reverseLines',
|
|
69
|
+
'uniqueLines',
|
|
70
|
+
'convertIndentsToSpaces',
|
|
71
|
+
'convertIndentsToTabs',
|
|
72
|
+
'hippieCompletion',
|
|
73
|
+
'hippieBackwardCompletion'
|
|
57
74
|
]);
|
|
58
75
|
|
|
76
|
+
const CODE_BLOCK_COMMANDS = new Set<EditorCommand>(['codeBlockStart', 'codeBlockEnd', 'codeBlockStartWithSelection', 'codeBlockEndWithSelection']);
|
|
77
|
+
|
|
59
78
|
const VIEW_COMMANDS = new Set<EditorViewCommand>([
|
|
60
79
|
'collapseRegion',
|
|
61
80
|
'expandRegion',
|
|
@@ -71,12 +90,30 @@ const VIEW_COMMANDS = new Set<EditorViewCommand>([
|
|
|
71
90
|
'expandAllToLevel3',
|
|
72
91
|
'expandAllToLevel4',
|
|
73
92
|
'expandAllToLevel5',
|
|
74
|
-
'toggleColumnMode'
|
|
93
|
+
'toggleColumnMode',
|
|
94
|
+
'increaseFontSize',
|
|
95
|
+
'decreaseFontSize',
|
|
96
|
+
'resetFontSize',
|
|
97
|
+
'methodUp',
|
|
98
|
+
'methodDown',
|
|
99
|
+
'nextChange',
|
|
100
|
+
'previousChange',
|
|
101
|
+
'pasteAsPlainText'
|
|
75
102
|
]);
|
|
76
103
|
|
|
104
|
+
const ZOOM_STEPS = { increaseFontSize: 1, decreaseFontSize: -1, resetFontSize: 0 } as const satisfies Partial<Record<EditorViewCommand, -1 | 0 | 1>>;
|
|
105
|
+
|
|
77
106
|
/* The clipboard text of the last copy or cut of a bare caret, which pastes back as whole lines. Shared, since it can be pasted in another editor. */
|
|
78
107
|
let lineClip: string | null = null;
|
|
79
108
|
|
|
109
|
+
/*
|
|
110
|
+
* Makes `text` the copy that pastes back as whole lines (or not), for a paste from a history that kept what the
|
|
111
|
+
* copy was. The clipboard itself is the caller's.
|
|
112
|
+
*/
|
|
113
|
+
export function rememberCopy(text: string, wholeLines: boolean): void {
|
|
114
|
+
lineClip = wholeLines ? text : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
80
117
|
function selectionSignature(selections: readonly Selection[]): string {
|
|
81
118
|
return selections.map((selection) => `${selection.anchor}:${selection.head}`).join(',');
|
|
82
119
|
}
|
|
@@ -96,6 +133,9 @@ export class InputController {
|
|
|
96
133
|
private readonly touch: TouchSelection;
|
|
97
134
|
private readonly cleanup: (() => void)[] = [];
|
|
98
135
|
private readonly keyHandlers = new Set<EditorKeyHandler>();
|
|
136
|
+
private readonly navigateListeners = new Set<(from: number) => void>();
|
|
137
|
+
private readonly copyListeners = new Set<(copy: EditorCopy) => void>();
|
|
138
|
+
private wordSources: (() => readonly string[]) | null = null;
|
|
99
139
|
private readonly gesture: DoubleModifierGesture;
|
|
100
140
|
private readonly clickHandlers = new Set<EditorClickHandler>();
|
|
101
141
|
private readonly inlayHandlers = new Set<EditorInlayClickHandler>();
|
|
@@ -109,6 +149,7 @@ export class InputController {
|
|
|
109
149
|
/* The Extend Selection presses that wait for a server's ranges, which are answered one after the other. */
|
|
110
150
|
private expansions: Promise<void> = Promise.resolve();
|
|
111
151
|
private lastInputAt = 0;
|
|
152
|
+
private readonly wheelZoom = new WheelZoom();
|
|
112
153
|
private desiredXs: number[] = [];
|
|
113
154
|
private keepDesiredXs = false;
|
|
114
155
|
private readonly view: EditorView;
|
|
@@ -224,13 +265,13 @@ export class InputController {
|
|
|
224
265
|
|
|
225
266
|
this.listen(input, 'paste', (event) => {
|
|
226
267
|
event.preventDefault();
|
|
227
|
-
this.
|
|
268
|
+
this.pasteText(event.clipboardData?.getData('text/plain') ?? '');
|
|
228
269
|
});
|
|
229
270
|
|
|
230
271
|
this.listen(input, 'copy', (event) => this.copy(event, false));
|
|
231
272
|
this.listen(input, 'cut', (event) => this.copy(event, true));
|
|
232
273
|
this.listen(viewport, 'scroll', () => this.view.scrolled());
|
|
233
|
-
this.listen(viewport, 'wheel', () => this.
|
|
274
|
+
this.listen(viewport, 'wheel', (event) => this.wheel(event));
|
|
234
275
|
this.listen(viewport, 'pointermove', (event) => this.view.hoverMoved(event));
|
|
235
276
|
|
|
236
277
|
this.listen(viewport, 'pointerleave', () => {
|
|
@@ -296,6 +337,8 @@ export class InputController {
|
|
|
296
337
|
|
|
297
338
|
this.cleanup.length = 0;
|
|
298
339
|
this.keyHandlers.clear();
|
|
340
|
+
this.navigateListeners.clear();
|
|
341
|
+
this.copyListeners.clear();
|
|
299
342
|
}
|
|
300
343
|
|
|
301
344
|
private blockedByReadOnly(): boolean {
|
|
@@ -340,16 +383,30 @@ export class InputController {
|
|
|
340
383
|
}
|
|
341
384
|
}
|
|
342
385
|
|
|
343
|
-
|
|
386
|
+
/* Pastes as a paste from the clipboard does; false when a read-only editor refused. */
|
|
387
|
+
pasteText(text: string, options: EditorPasteOptions = {}): boolean {
|
|
344
388
|
if (this.blockedByReadOnly()) {
|
|
345
|
-
return;
|
|
389
|
+
return false;
|
|
346
390
|
}
|
|
347
391
|
|
|
348
392
|
this.unfoldCarets();
|
|
349
393
|
this.historyGroup++;
|
|
350
|
-
|
|
394
|
+
|
|
395
|
+
const pasted = this.view.model.paste(this.native.normalizeNewlines(text), {
|
|
396
|
+
...this.commandOptions(),
|
|
397
|
+
...(options.reindent === false ? { indentOnPaste: false } : {}),
|
|
398
|
+
wholeLines: options.wholeLines ?? (text !== '' && text === lineClip)
|
|
399
|
+
});
|
|
400
|
+
|
|
351
401
|
this.historyGroup++;
|
|
352
402
|
this.view.revealCaret();
|
|
403
|
+
|
|
404
|
+
return pasted;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Told what a copy or cut put on the clipboard. */
|
|
408
|
+
onCopy(listener: (copy: EditorCopy) => void): () => void {
|
|
409
|
+
return subscribe(this.copyListeners, listener);
|
|
353
410
|
}
|
|
354
411
|
|
|
355
412
|
private commandOptions(): CommandOptions {
|
|
@@ -365,24 +422,70 @@ export class InputController {
|
|
|
365
422
|
tabOutOfClosers: keys.tabOutOfClosers,
|
|
366
423
|
smartEnter: keys.smartIndentOnEnter,
|
|
367
424
|
indentOnPaste: keys.indentOnPaste,
|
|
425
|
+
indentGuides: settings.guides,
|
|
368
426
|
smartSemicolon: keys.smartSemicolon,
|
|
369
427
|
smartArrow: keys.smartArrow,
|
|
370
428
|
language: settings.language ?? 'plaintext',
|
|
371
429
|
camelCase: keys.camelHumps,
|
|
372
430
|
visualLine: (offset) => this.view.rowOf(offset),
|
|
373
|
-
lineSpan: (line) => this.view.lineSpan(line)
|
|
431
|
+
lineSpan: (line) => this.view.lineSpan(line),
|
|
432
|
+
structure: () => this.view.structureRanges(),
|
|
433
|
+
...(this.wordSources === null ? {} : { wordSources: this.wordSources })
|
|
374
434
|
};
|
|
375
435
|
}
|
|
376
436
|
|
|
437
|
+
/* The texts Hippie completion takes words from after those of this document; null takes them away. */
|
|
438
|
+
setWordSources(source: (() => readonly string[]) | null): void {
|
|
439
|
+
this.wordSources = source;
|
|
440
|
+
}
|
|
441
|
+
|
|
377
442
|
/* Folding is the view's, since it knows where the ranges are. */
|
|
378
443
|
private viewCommand(name: EditorViewCommand): boolean {
|
|
379
444
|
const { view } = this;
|
|
380
445
|
|
|
446
|
+
if (name in ZOOM_STEPS) {
|
|
447
|
+
this.options.zoom(ZOOM_STEPS[name as keyof typeof ZOOM_STEPS]);
|
|
448
|
+
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (name === 'methodUp' || name === 'methodDown') {
|
|
453
|
+
const from = view.model.getPrimary().head;
|
|
454
|
+
const moved = view.goToMethod(name === 'methodUp' ? -1 : 1);
|
|
455
|
+
|
|
456
|
+
if (moved) {
|
|
457
|
+
emit(this.navigateListeners, from);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return moved;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (name === 'pasteAsPlainText') {
|
|
464
|
+
// The clipboard is read once a person asks, and an editor with no way to read it does nothing.
|
|
465
|
+
if (this.blockedByReadOnly() || globalThis.navigator?.clipboard?.readText === undefined) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
void navigator.clipboard.readText().then(
|
|
470
|
+
(text) => this.pasteText(text, { reindent: false }),
|
|
471
|
+
() => undefined
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (name === 'nextChange' || name === 'previousChange') {
|
|
478
|
+
return view.goToChange(name === 'nextChange' ? 1 : -1);
|
|
479
|
+
}
|
|
480
|
+
|
|
381
481
|
if (view.settings.folding === false && name !== 'toggleColumnMode') {
|
|
382
482
|
return false;
|
|
383
483
|
}
|
|
384
484
|
|
|
385
|
-
const run: Record<
|
|
485
|
+
const run: Record<
|
|
486
|
+
Exclude<EditorViewCommand, keyof typeof ZOOM_STEPS | 'methodUp' | 'methodDown' | 'nextChange' | 'previousChange' | 'pasteAsPlainText'>,
|
|
487
|
+
() => void
|
|
488
|
+
> = {
|
|
386
489
|
collapseRegion: () => view.collapseRegion(),
|
|
387
490
|
expandRegion: () => view.expandRegion(),
|
|
388
491
|
collapseAllRegions: () => view.foldAll(true),
|
|
@@ -400,11 +503,27 @@ export class InputController {
|
|
|
400
503
|
toggleColumnMode: () => this.toggleColumnMode()
|
|
401
504
|
};
|
|
402
505
|
|
|
403
|
-
run[name]();
|
|
506
|
+
run[name as keyof typeof run]();
|
|
404
507
|
|
|
405
508
|
return true;
|
|
406
509
|
}
|
|
407
510
|
|
|
511
|
+
/* A pinch comes as the wheel with Ctrl, on every platform. */
|
|
512
|
+
private wheel(event: WheelEvent): void {
|
|
513
|
+
this.view.cancelScroll();
|
|
514
|
+
|
|
515
|
+
if (!(event.ctrlKey || (this.options.apple && event.metaKey)) || !this.options.wheelZooms()) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
event.preventDefault();
|
|
520
|
+
const step = this.wheelZoom.step(event);
|
|
521
|
+
|
|
522
|
+
if (step !== 0) {
|
|
523
|
+
this.options.zoom(step as -1 | 1);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
408
527
|
/* Runs a model command on every caret, with the folds that would hide its result opened first. */
|
|
409
528
|
command(name: EditorRunCommand): boolean {
|
|
410
529
|
if (VIEW_COMMANDS.has(name as EditorViewCommand)) {
|
|
@@ -491,7 +610,11 @@ export class InputController {
|
|
|
491
610
|
}
|
|
492
611
|
}
|
|
493
612
|
|
|
494
|
-
const
|
|
613
|
+
const before = model.getPrimary().head;
|
|
614
|
+
const folded =
|
|
615
|
+
name === 'moveLineUp' || name === 'moveLineDown' || name === 'moveStatementUp' || name === 'moveStatementDown'
|
|
616
|
+
? this.view.getFolds().collapsed
|
|
617
|
+
: [];
|
|
495
618
|
|
|
496
619
|
const result = model.execute(name, {
|
|
497
620
|
...this.commandOptions(),
|
|
@@ -506,11 +629,24 @@ export class InputController {
|
|
|
506
629
|
this.view.markRowEnd(model.getPrimary().head);
|
|
507
630
|
}
|
|
508
631
|
|
|
632
|
+
if (name === 'hippieCompletion' || name === 'hippieBackwardCompletion') {
|
|
633
|
+
this.view.setHippieSource(model.hippieSource);
|
|
634
|
+
}
|
|
635
|
+
|
|
509
636
|
this.view.revealCaret();
|
|
510
637
|
|
|
638
|
+
if (CODE_BLOCK_COMMANDS.has(name) && model.getPrimary().head !== before) {
|
|
639
|
+
emit(this.navigateListeners, before);
|
|
640
|
+
}
|
|
641
|
+
|
|
511
642
|
return result;
|
|
512
643
|
}
|
|
513
644
|
|
|
645
|
+
/* Told the offset the primary caret left when a command that navigates moved it. */
|
|
646
|
+
onNavigate(listener: (from: number) => void): () => void {
|
|
647
|
+
return subscribe(this.navigateListeners, listener);
|
|
648
|
+
}
|
|
649
|
+
|
|
514
650
|
private typeText(text: string): void {
|
|
515
651
|
if (this.blockedByReadOnly()) {
|
|
516
652
|
return;
|
|
@@ -799,6 +935,8 @@ export class InputController {
|
|
|
799
935
|
return;
|
|
800
936
|
}
|
|
801
937
|
|
|
938
|
+
this.view.setHippieSource(null);
|
|
939
|
+
|
|
802
940
|
const added = this.gesture.keydown(event);
|
|
803
941
|
|
|
804
942
|
if (added !== null) {
|
|
@@ -895,6 +1033,7 @@ export class InputController {
|
|
|
895
1033
|
|
|
896
1034
|
event.clipboardData?.setData('text/plain', text);
|
|
897
1035
|
lineClip = empty ? text : null;
|
|
1036
|
+
emit(this.copyListeners, { text, wholeLines: empty, cut });
|
|
898
1037
|
|
|
899
1038
|
if (!cut || this.blockedByReadOnly()) {
|
|
900
1039
|
return;
|
package/src/editor.css
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
--editor-added: var(--status-idle);
|
|
18
18
|
--editor-modified: var(--accent);
|
|
19
19
|
--editor-deleted: var(--status-error);
|
|
20
|
+
--editor-todo: var(--status-running);
|
|
20
21
|
--editor-mark-number-mix: 72%;
|
|
21
22
|
--editor-mark-bar-width: 3px;
|
|
22
23
|
--editor-mark-gap: 4px;
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
--editor-margin: color-mix(in srgb, var(--text) 16%, transparent);
|
|
39
40
|
--editor-whitespace: color-mix(in srgb, var(--text) 32%, transparent);
|
|
40
41
|
--editor-hatch: color-mix(in srgb, var(--text) 10%, transparent);
|
|
42
|
+
--editor-todo: var(--status-idle);
|
|
41
43
|
--editor-mark-number-mix: 82%;
|
|
42
44
|
--editor-sticky-shadow: color-mix(in srgb, black 60%, transparent);
|
|
43
45
|
}
|
|
@@ -478,11 +480,15 @@
|
|
|
478
480
|
left: 6px;
|
|
479
481
|
background: var(--editor-occurrence);
|
|
480
482
|
}
|
|
483
|
+
.se-tick-todo,
|
|
481
484
|
.se-tick-error,
|
|
482
485
|
.se-tick-warning,
|
|
483
486
|
.se-tick-info {
|
|
484
487
|
left: 10px;
|
|
485
488
|
}
|
|
489
|
+
.se-tick-todo {
|
|
490
|
+
background: var(--editor-todo);
|
|
491
|
+
}
|
|
486
492
|
/* The only part of the track that takes the pointer, with a wider reach than the 4px it draws. */
|
|
487
493
|
.se-tick[data-offset] {
|
|
488
494
|
pointer-events: auto;
|
|
@@ -646,11 +652,16 @@
|
|
|
646
652
|
.se-find-match,
|
|
647
653
|
.se-find-current,
|
|
648
654
|
.se-occurrence,
|
|
655
|
+
.se-hippie-source,
|
|
649
656
|
.se-bracket-match,
|
|
650
657
|
.se-bracket-unmatched {
|
|
651
658
|
position: absolute;
|
|
652
659
|
border-radius: var(--radius-xs);
|
|
653
660
|
}
|
|
661
|
+
.se-hippie-source {
|
|
662
|
+
background: color-mix(in srgb, var(--accent) 22%, transparent);
|
|
663
|
+
border-radius: var(--radius-sm);
|
|
664
|
+
}
|
|
654
665
|
.se-find-match {
|
|
655
666
|
background: var(--find-match);
|
|
656
667
|
}
|
|
@@ -741,6 +752,14 @@
|
|
|
741
752
|
white-space: pre;
|
|
742
753
|
cursor: default;
|
|
743
754
|
}
|
|
755
|
+
.se-swatch {
|
|
756
|
+
height: 12px;
|
|
757
|
+
margin-top: calc((var(--code-line-height, 20px) - 12px) / 2);
|
|
758
|
+
padding: 0;
|
|
759
|
+
border-radius: var(--radius-sm);
|
|
760
|
+
box-shadow: inset 0 0 0 1px var(--border);
|
|
761
|
+
cursor: pointer;
|
|
762
|
+
}
|
|
744
763
|
.se-inlay-link {
|
|
745
764
|
text-decoration: underline;
|
|
746
765
|
text-underline-offset: 2px;
|
package/src/engine.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
EditorCodeVision,
|
|
18
18
|
EditorClickHandler,
|
|
19
19
|
EditorContextMenu,
|
|
20
|
+
EditorCopy,
|
|
20
21
|
EditorContentChange,
|
|
21
22
|
EditorEngine,
|
|
22
23
|
EditorFindQuery,
|
|
@@ -36,6 +37,7 @@ import type {
|
|
|
36
37
|
EditorInlayHint,
|
|
37
38
|
EditorInlayTarget,
|
|
38
39
|
EditorKeyHandler,
|
|
40
|
+
EditorPasteOptions,
|
|
39
41
|
EditorInputPopup,
|
|
40
42
|
EditorMarker,
|
|
41
43
|
EditorMessages,
|
|
@@ -53,11 +55,13 @@ import type {
|
|
|
53
55
|
EditorTextChange,
|
|
54
56
|
EditorTrackedRange,
|
|
55
57
|
EditorTheme,
|
|
58
|
+
EditorWhitespaceCleanup,
|
|
56
59
|
EditorWidget,
|
|
57
60
|
SmartEditorEngineOptions
|
|
58
61
|
} from './types.ts';
|
|
59
62
|
import { fillLens, lensSignature } from './lens.ts';
|
|
60
63
|
import { EditorView, type ViewSettings } from './view.ts';
|
|
64
|
+
import { clampFontSize, type FontSizeRange, fontSizeRange } from './zoom.ts';
|
|
61
65
|
|
|
62
66
|
const DEFAULT_TAB_SIZE = 4;
|
|
63
67
|
const DEFAULT_LABEL = 'Code editor';
|
|
@@ -77,6 +81,9 @@ class SmartEditor implements Editor {
|
|
|
77
81
|
private caretOffset = 0;
|
|
78
82
|
private readonly saves = new Set<Listener>();
|
|
79
83
|
private readonly blurs = new Set<Listener>();
|
|
84
|
+
private readonly fontSizes = new Set<(size: number | null) => void>();
|
|
85
|
+
private readonly fontSizeRange: FontSizeRange;
|
|
86
|
+
private fontSize: number | null = null;
|
|
80
87
|
private readonly subscription: { dispose(): void };
|
|
81
88
|
private language: string | undefined;
|
|
82
89
|
private theme: EditorTheme;
|
|
@@ -85,12 +92,15 @@ class SmartEditor implements Editor {
|
|
|
85
92
|
private lensRevisions = new Map<string, { signature: string; revision: number }>();
|
|
86
93
|
private tokenizerRequest = 0;
|
|
87
94
|
private settingText = false;
|
|
95
|
+
/* The hints of each owner, in the order the owners first set theirs. */
|
|
96
|
+
private readonly inlayOwners = new Map<string, readonly EditorInlayHint[]>();
|
|
88
97
|
private disposed = false;
|
|
89
98
|
|
|
90
99
|
private readonly engine: SmartEditorEngineOptions;
|
|
91
100
|
|
|
92
101
|
constructor(engine: SmartEditorEngineOptions, element: HTMLElement, options: EditorOptions) {
|
|
93
102
|
this.engine = engine;
|
|
103
|
+
this.fontSizeRange = fontSizeRange(options.zoom);
|
|
94
104
|
this.model = new DocumentModel(options.text);
|
|
95
105
|
this.language = options.language;
|
|
96
106
|
this.theme = options.theme;
|
|
@@ -111,7 +121,8 @@ class SmartEditor implements Editor {
|
|
|
111
121
|
whitespace: options.whitespace === true,
|
|
112
122
|
rightMargin: options.rightMargin ?? null,
|
|
113
123
|
foldOutline: options.foldOutline ?? 'hover',
|
|
114
|
-
folding: options.folding !== false
|
|
124
|
+
folding: options.folding !== false,
|
|
125
|
+
todoWords: options.todoWords
|
|
115
126
|
};
|
|
116
127
|
|
|
117
128
|
this.view = new EditorView(element, this.model, this.settings);
|
|
@@ -122,9 +133,15 @@ class SmartEditor implements Editor {
|
|
|
122
133
|
keymap: engine.keymap,
|
|
123
134
|
apple: engine.apple ?? false,
|
|
124
135
|
save: () => emit(this.saves),
|
|
125
|
-
blur: () => emit(this.blurs)
|
|
136
|
+
blur: () => emit(this.blurs),
|
|
137
|
+
zoom: (step) => this.zoom(step),
|
|
138
|
+
wheelZooms: () => options.zoom?.wheel !== false
|
|
126
139
|
});
|
|
127
140
|
|
|
141
|
+
if (options.fontSize !== undefined) {
|
|
142
|
+
this.setFontSize(options.fontSize);
|
|
143
|
+
}
|
|
144
|
+
|
|
128
145
|
this.revision = this.model.getRevision();
|
|
129
146
|
this.caretOffset = this.model.getPrimary().head;
|
|
130
147
|
|
|
@@ -392,6 +409,17 @@ class SmartEditor implements Editor {
|
|
|
392
409
|
}
|
|
393
410
|
}
|
|
394
411
|
|
|
412
|
+
cleanUpWhitespace(options: EditorWhitespaceCleanup): boolean {
|
|
413
|
+
const changed = !this.settings.readOnly && this.model.cleanUpWhitespace({ ...options, language: this.settings.language });
|
|
414
|
+
this.model.markSaved();
|
|
415
|
+
|
|
416
|
+
return changed;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
markSaved(): void {
|
|
420
|
+
this.model.markSaved();
|
|
421
|
+
}
|
|
422
|
+
|
|
395
423
|
setMarkers(markers: readonly EditorMarker[]): void {
|
|
396
424
|
this.view.setMarkers(
|
|
397
425
|
markers.map((marker) => ({
|
|
@@ -405,15 +433,21 @@ class SmartEditor implements Editor {
|
|
|
405
433
|
);
|
|
406
434
|
}
|
|
407
435
|
|
|
408
|
-
setInlayHints(hints: readonly EditorInlayHint[]): void {
|
|
436
|
+
setInlayHints(hints: readonly EditorInlayHint[], owner: string = DEFAULT_WIDGET_OWNER): void {
|
|
437
|
+
if (hints.length === 0) {
|
|
438
|
+
this.inlayOwners.delete(owner);
|
|
439
|
+
} else {
|
|
440
|
+
this.inlayOwners.set(owner, hints);
|
|
441
|
+
}
|
|
409
442
|
this.view.setInlays(
|
|
410
|
-
|
|
443
|
+
[...this.inlayOwners.values()].flat().map((hint, index) => ({
|
|
411
444
|
id: hint.id ?? `hint-${index}`,
|
|
412
445
|
at: this.offsetAt(hint.position),
|
|
413
446
|
text: hint.label,
|
|
414
447
|
...(hint.parts === undefined ? {} : { parts: hint.parts }),
|
|
415
448
|
...(hint.paddingLeft ? { gapLeft: INLAY_GAP } : {}),
|
|
416
|
-
...(hint.paddingRight ? { gapRight: INLAY_GAP } : {})
|
|
449
|
+
...(hint.paddingRight ? { gapRight: INLAY_GAP } : {}),
|
|
450
|
+
...(hint.swatch === undefined ? {} : { swatch: hint.swatch })
|
|
417
451
|
}))
|
|
418
452
|
);
|
|
419
453
|
}
|
|
@@ -905,10 +939,39 @@ class SmartEditor implements Editor {
|
|
|
905
939
|
this.view.applySettings();
|
|
906
940
|
}
|
|
907
941
|
|
|
942
|
+
setTodoWords(words: readonly string[]): void {
|
|
943
|
+
this.settings.todoWords = [...words];
|
|
944
|
+
this.view.applySettings();
|
|
945
|
+
}
|
|
946
|
+
|
|
908
947
|
setFoldHints(hints: EditorFoldHints | null): void {
|
|
909
948
|
this.view.setFoldHints(hints === null ? null : this.foldHintsByOffset(hints));
|
|
910
949
|
}
|
|
911
950
|
|
|
951
|
+
setDeclarations(positions: readonly EditorPosition[] | null): void {
|
|
952
|
+
this.view.setDeclarations(positions === null ? null : positions.map((position) => this.offsetAt(position)));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
onNavigate(listener: (from: EditorPosition) => void): () => void {
|
|
956
|
+
return this.controller.onNavigate((from) => listener(this.positionAt(from)));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
hasFocus(): boolean {
|
|
960
|
+
return this.view.focused;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
setWordSources(source: (() => readonly string[]) | null): void {
|
|
964
|
+
this.controller.setWordSources(source);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
onCopy(listener: (copy: EditorCopy) => void): () => void {
|
|
968
|
+
return this.controller.onCopy(listener);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
paste(text: string, options?: EditorPasteOptions): boolean {
|
|
972
|
+
return this.controller.pasteText(text, options);
|
|
973
|
+
}
|
|
974
|
+
|
|
912
975
|
private foldHintsByOffset(hints: EditorFoldHints): FoldHints {
|
|
913
976
|
const lineOf = (line: number): { start: number; end: number } => this.model.getLine(Math.min(line, this.model.getLineCount() - 1));
|
|
914
977
|
|
|
@@ -960,6 +1023,40 @@ class SmartEditor implements Editor {
|
|
|
960
1023
|
this.view.refreshFont();
|
|
961
1024
|
}
|
|
962
1025
|
|
|
1026
|
+
setFontSize(size: number | null): void {
|
|
1027
|
+
const next = size === null || !Number.isFinite(size) ? null : clampFontSize(size, this.fontSizeRange);
|
|
1028
|
+
|
|
1029
|
+
if (next !== this.fontSize) {
|
|
1030
|
+
this.fontSize = next;
|
|
1031
|
+
this.view.setFontSize(next);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
getFontSize(): number {
|
|
1036
|
+
return this.view.getFontSize();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
onFontSizeChange(listener: (size: number | null) => void): () => void {
|
|
1040
|
+
return subscribe(this.fontSizes, listener);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/* Steps from the size on screen, so the first step from the page's size is one pixel away from it. */
|
|
1044
|
+
private zoom(step: -1 | 0 | 1): void {
|
|
1045
|
+
const before = this.fontSize;
|
|
1046
|
+
const current = this.view.getFontSize();
|
|
1047
|
+
|
|
1048
|
+
// A page size outside the range must not make a step the other way.
|
|
1049
|
+
if (step !== 0 && Math.sign(clampFontSize(current + step, this.fontSizeRange) - current) !== step) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
this.setFontSize(step === 0 ? null : current + step);
|
|
1054
|
+
|
|
1055
|
+
if (this.fontSize !== before) {
|
|
1056
|
+
emit(this.fontSizes, this.fontSize);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
963
1060
|
setReadOnly(readOnly: boolean, reason?: string): void {
|
|
964
1061
|
this.settings.readOnly = readOnly;
|
|
965
1062
|
this.settings.readOnlyReason = reason;
|
|
@@ -994,6 +1091,7 @@ class SmartEditor implements Editor {
|
|
|
994
1091
|
this.carets.clear();
|
|
995
1092
|
this.saves.clear();
|
|
996
1093
|
this.blurs.clear();
|
|
1094
|
+
this.fontSizes.clear();
|
|
997
1095
|
this.subscription.dispose();
|
|
998
1096
|
this.controller.dispose();
|
|
999
1097
|
this.view.dispose();
|