@adecore/editor 0.42.0 → 0.44.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 +9 -2
- package/dist/controller.js +82 -8
- package/dist/editor.css +13 -0
- package/dist/engine.js +44 -3
- package/dist/fake.d.ts +23 -2
- package/dist/fake.js +80 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/keymap-table.d.ts +25 -1
- package/dist/keymap-table.js +30 -3
- package/dist/keymap.js +14 -0
- 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/paint.js +12 -2
- package/dist/text-queries.d.ts +10 -0
- package/dist/text-queries.js +24 -0
- package/dist/types.d.ts +41 -3
- package/dist/view.d.ts +11 -0
- package/dist/view.js +73 -8
- package/package.json +2 -2
- package/src/controller.ts +110 -9
- package/src/editor.css +13 -0
- package/src/engine.ts +60 -3
- package/src/fake.ts +109 -4
- package/src/index.ts +8 -0
- package/src/keymap-table.ts +55 -4
- package/src/keymap.ts +14 -0
- package/src/layout.ts +9 -1
- package/src/outline.ts +13 -0
- package/src/paint.ts +11 -2
- package/src/text-queries.ts +45 -0
- package/src/types.ts +77 -3
- package/src/view.ts +95 -9
package/src/controller.ts
CHANGED
|
@@ -15,7 +15,9 @@ import type {
|
|
|
15
15
|
EditorInlayClick,
|
|
16
16
|
EditorInlayClickHandler,
|
|
17
17
|
EditorContextMenu,
|
|
18
|
+
EditorCopy,
|
|
18
19
|
EditorKeyHandler,
|
|
20
|
+
EditorPasteOptions,
|
|
19
21
|
EditorPosition,
|
|
20
22
|
EditorRunCommand,
|
|
21
23
|
EditorSelectionRanges,
|
|
@@ -45,9 +47,12 @@ const MODIFYING = new Set<EditorCommand>([
|
|
|
45
47
|
'deleteLine',
|
|
46
48
|
'moveLineUp',
|
|
47
49
|
'moveLineDown',
|
|
50
|
+
'moveStatementUp',
|
|
51
|
+
'moveStatementDown',
|
|
48
52
|
'toggleLineComment',
|
|
49
53
|
'toggleBlockComment',
|
|
50
54
|
'startNewLine',
|
|
55
|
+
'completeCurrentStatement',
|
|
51
56
|
'startNewLineBefore',
|
|
52
57
|
'splitLine',
|
|
53
58
|
'joinLines',
|
|
@@ -63,9 +68,13 @@ const MODIFYING = new Set<EditorCommand>([
|
|
|
63
68
|
'reverseLines',
|
|
64
69
|
'uniqueLines',
|
|
65
70
|
'convertIndentsToSpaces',
|
|
66
|
-
'convertIndentsToTabs'
|
|
71
|
+
'convertIndentsToTabs',
|
|
72
|
+
'hippieCompletion',
|
|
73
|
+
'hippieBackwardCompletion'
|
|
67
74
|
]);
|
|
68
75
|
|
|
76
|
+
const CODE_BLOCK_COMMANDS = new Set<EditorCommand>(['codeBlockStart', 'codeBlockEnd', 'codeBlockStartWithSelection', 'codeBlockEndWithSelection']);
|
|
77
|
+
|
|
69
78
|
const VIEW_COMMANDS = new Set<EditorViewCommand>([
|
|
70
79
|
'collapseRegion',
|
|
71
80
|
'expandRegion',
|
|
@@ -84,7 +93,12 @@ const VIEW_COMMANDS = new Set<EditorViewCommand>([
|
|
|
84
93
|
'toggleColumnMode',
|
|
85
94
|
'increaseFontSize',
|
|
86
95
|
'decreaseFontSize',
|
|
87
|
-
'resetFontSize'
|
|
96
|
+
'resetFontSize',
|
|
97
|
+
'methodUp',
|
|
98
|
+
'methodDown',
|
|
99
|
+
'nextChange',
|
|
100
|
+
'previousChange',
|
|
101
|
+
'pasteAsPlainText'
|
|
88
102
|
]);
|
|
89
103
|
|
|
90
104
|
const ZOOM_STEPS = { increaseFontSize: 1, decreaseFontSize: -1, resetFontSize: 0 } as const satisfies Partial<Record<EditorViewCommand, -1 | 0 | 1>>;
|
|
@@ -92,6 +106,14 @@ const ZOOM_STEPS = { increaseFontSize: 1, decreaseFontSize: -1, resetFontSize: 0
|
|
|
92
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. */
|
|
93
107
|
let lineClip: string | null = null;
|
|
94
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
|
+
|
|
95
117
|
function selectionSignature(selections: readonly Selection[]): string {
|
|
96
118
|
return selections.map((selection) => `${selection.anchor}:${selection.head}`).join(',');
|
|
97
119
|
}
|
|
@@ -111,6 +133,9 @@ export class InputController {
|
|
|
111
133
|
private readonly touch: TouchSelection;
|
|
112
134
|
private readonly cleanup: (() => void)[] = [];
|
|
113
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;
|
|
114
139
|
private readonly gesture: DoubleModifierGesture;
|
|
115
140
|
private readonly clickHandlers = new Set<EditorClickHandler>();
|
|
116
141
|
private readonly inlayHandlers = new Set<EditorInlayClickHandler>();
|
|
@@ -240,7 +265,7 @@ export class InputController {
|
|
|
240
265
|
|
|
241
266
|
this.listen(input, 'paste', (event) => {
|
|
242
267
|
event.preventDefault();
|
|
243
|
-
this.
|
|
268
|
+
this.pasteText(event.clipboardData?.getData('text/plain') ?? '');
|
|
244
269
|
});
|
|
245
270
|
|
|
246
271
|
this.listen(input, 'copy', (event) => this.copy(event, false));
|
|
@@ -312,6 +337,8 @@ export class InputController {
|
|
|
312
337
|
|
|
313
338
|
this.cleanup.length = 0;
|
|
314
339
|
this.keyHandlers.clear();
|
|
340
|
+
this.navigateListeners.clear();
|
|
341
|
+
this.copyListeners.clear();
|
|
315
342
|
}
|
|
316
343
|
|
|
317
344
|
private blockedByReadOnly(): boolean {
|
|
@@ -356,16 +383,30 @@ export class InputController {
|
|
|
356
383
|
}
|
|
357
384
|
}
|
|
358
385
|
|
|
359
|
-
|
|
386
|
+
/* Pastes as a paste from the clipboard does; false when a read-only editor refused. */
|
|
387
|
+
pasteText(text: string, options: EditorPasteOptions = {}): boolean {
|
|
360
388
|
if (this.blockedByReadOnly()) {
|
|
361
|
-
return;
|
|
389
|
+
return false;
|
|
362
390
|
}
|
|
363
391
|
|
|
364
392
|
this.unfoldCarets();
|
|
365
393
|
this.historyGroup++;
|
|
366
|
-
|
|
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
|
+
|
|
367
401
|
this.historyGroup++;
|
|
368
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);
|
|
369
410
|
}
|
|
370
411
|
|
|
371
412
|
private commandOptions(): CommandOptions {
|
|
@@ -381,15 +422,23 @@ export class InputController {
|
|
|
381
422
|
tabOutOfClosers: keys.tabOutOfClosers,
|
|
382
423
|
smartEnter: keys.smartIndentOnEnter,
|
|
383
424
|
indentOnPaste: keys.indentOnPaste,
|
|
425
|
+
indentGuides: settings.guides,
|
|
384
426
|
smartSemicolon: keys.smartSemicolon,
|
|
385
427
|
smartArrow: keys.smartArrow,
|
|
386
428
|
language: settings.language ?? 'plaintext',
|
|
387
429
|
camelCase: keys.camelHumps,
|
|
388
430
|
visualLine: (offset) => this.view.rowOf(offset),
|
|
389
|
-
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 })
|
|
390
434
|
};
|
|
391
435
|
}
|
|
392
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
|
+
|
|
393
442
|
/* Folding is the view's, since it knows where the ranges are. */
|
|
394
443
|
private viewCommand(name: EditorViewCommand): boolean {
|
|
395
444
|
const { view } = this;
|
|
@@ -400,11 +449,43 @@ export class InputController {
|
|
|
400
449
|
return true;
|
|
401
450
|
}
|
|
402
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
|
+
|
|
403
481
|
if (view.settings.folding === false && name !== 'toggleColumnMode') {
|
|
404
482
|
return false;
|
|
405
483
|
}
|
|
406
484
|
|
|
407
|
-
const run: Record<
|
|
485
|
+
const run: Record<
|
|
486
|
+
Exclude<EditorViewCommand, keyof typeof ZOOM_STEPS | 'methodUp' | 'methodDown' | 'nextChange' | 'previousChange' | 'pasteAsPlainText'>,
|
|
487
|
+
() => void
|
|
488
|
+
> = {
|
|
408
489
|
collapseRegion: () => view.collapseRegion(),
|
|
409
490
|
expandRegion: () => view.expandRegion(),
|
|
410
491
|
collapseAllRegions: () => view.foldAll(true),
|
|
@@ -529,7 +610,11 @@ export class InputController {
|
|
|
529
610
|
}
|
|
530
611
|
}
|
|
531
612
|
|
|
532
|
-
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
|
+
: [];
|
|
533
618
|
|
|
534
619
|
const result = model.execute(name, {
|
|
535
620
|
...this.commandOptions(),
|
|
@@ -544,11 +629,24 @@ export class InputController {
|
|
|
544
629
|
this.view.markRowEnd(model.getPrimary().head);
|
|
545
630
|
}
|
|
546
631
|
|
|
632
|
+
if (name === 'hippieCompletion' || name === 'hippieBackwardCompletion') {
|
|
633
|
+
this.view.setHippieSource(model.hippieSource);
|
|
634
|
+
}
|
|
635
|
+
|
|
547
636
|
this.view.revealCaret();
|
|
548
637
|
|
|
638
|
+
if (CODE_BLOCK_COMMANDS.has(name) && model.getPrimary().head !== before) {
|
|
639
|
+
emit(this.navigateListeners, before);
|
|
640
|
+
}
|
|
641
|
+
|
|
549
642
|
return result;
|
|
550
643
|
}
|
|
551
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
|
+
|
|
552
650
|
private typeText(text: string): void {
|
|
553
651
|
if (this.blockedByReadOnly()) {
|
|
554
652
|
return;
|
|
@@ -837,6 +935,8 @@ export class InputController {
|
|
|
837
935
|
return;
|
|
838
936
|
}
|
|
839
937
|
|
|
938
|
+
this.view.setHippieSource(null);
|
|
939
|
+
|
|
840
940
|
const added = this.gesture.keydown(event);
|
|
841
941
|
|
|
842
942
|
if (added !== null) {
|
|
@@ -933,6 +1033,7 @@ export class InputController {
|
|
|
933
1033
|
|
|
934
1034
|
event.clipboardData?.setData('text/plain', text);
|
|
935
1035
|
lineClip = empty ? text : null;
|
|
1036
|
+
emit(this.copyListeners, { text, wholeLines: empty, cut });
|
|
936
1037
|
|
|
937
1038
|
if (!cut || this.blockedByReadOnly()) {
|
|
938
1039
|
return;
|
package/src/editor.css
CHANGED
|
@@ -652,11 +652,16 @@
|
|
|
652
652
|
.se-find-match,
|
|
653
653
|
.se-find-current,
|
|
654
654
|
.se-occurrence,
|
|
655
|
+
.se-hippie-source,
|
|
655
656
|
.se-bracket-match,
|
|
656
657
|
.se-bracket-unmatched {
|
|
657
658
|
position: absolute;
|
|
658
659
|
border-radius: var(--radius-xs);
|
|
659
660
|
}
|
|
661
|
+
.se-hippie-source {
|
|
662
|
+
background: color-mix(in srgb, var(--accent) 22%, transparent);
|
|
663
|
+
border-radius: var(--radius-sm);
|
|
664
|
+
}
|
|
660
665
|
.se-find-match {
|
|
661
666
|
background: var(--find-match);
|
|
662
667
|
}
|
|
@@ -747,6 +752,14 @@
|
|
|
747
752
|
white-space: pre;
|
|
748
753
|
cursor: default;
|
|
749
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
|
+
}
|
|
750
763
|
.se-inlay-link {
|
|
751
764
|
text-decoration: underline;
|
|
752
765
|
text-underline-offset: 2px;
|
package/src/engine.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { changedSpans, DocumentModel, type EditorSnapshot, type FoldHints, replacementText } from '@adecore/editor-core';
|
|
2
2
|
import { MAX_REPLACEMENT_CHARACTERS } from './search-protocol.ts';
|
|
3
|
+
import { placeAt, postfixOperands, surroundShape, unwrapTargets } from './text-queries.ts';
|
|
3
4
|
import { GrammarPreparation } from './grammar-preparation.ts';
|
|
4
5
|
import { InputController } from './controller.ts';
|
|
5
6
|
import { resolveSmartKeys } from './smart-keys.ts';
|
|
@@ -17,6 +18,7 @@ import type {
|
|
|
17
18
|
EditorCodeVision,
|
|
18
19
|
EditorClickHandler,
|
|
19
20
|
EditorContextMenu,
|
|
21
|
+
EditorCopy,
|
|
20
22
|
EditorContentChange,
|
|
21
23
|
EditorEngine,
|
|
22
24
|
EditorFindQuery,
|
|
@@ -36,11 +38,16 @@ import type {
|
|
|
36
38
|
EditorInlayHint,
|
|
37
39
|
EditorInlayTarget,
|
|
38
40
|
EditorKeyHandler,
|
|
41
|
+
EditorPasteOptions,
|
|
39
42
|
EditorInputPopup,
|
|
40
43
|
EditorMarker,
|
|
41
44
|
EditorMessages,
|
|
42
45
|
EditorOptions,
|
|
43
46
|
EditorPosition,
|
|
47
|
+
EditorPostfixOperand,
|
|
48
|
+
EditorSurroundShape,
|
|
49
|
+
EditorTextPlace,
|
|
50
|
+
EditorUnwrapTarget,
|
|
44
51
|
EditorRemoteCursor,
|
|
45
52
|
EditorRange,
|
|
46
53
|
EditorReveal,
|
|
@@ -90,6 +97,8 @@ class SmartEditor implements Editor {
|
|
|
90
97
|
private lensRevisions = new Map<string, { signature: string; revision: number }>();
|
|
91
98
|
private tokenizerRequest = 0;
|
|
92
99
|
private settingText = false;
|
|
100
|
+
/* The hints of each owner, in the order the owners first set theirs. */
|
|
101
|
+
private readonly inlayOwners = new Map<string, readonly EditorInlayHint[]>();
|
|
93
102
|
private disposed = false;
|
|
94
103
|
|
|
95
104
|
private readonly engine: SmartEditorEngineOptions;
|
|
@@ -429,15 +438,21 @@ class SmartEditor implements Editor {
|
|
|
429
438
|
);
|
|
430
439
|
}
|
|
431
440
|
|
|
432
|
-
setInlayHints(hints: readonly EditorInlayHint[]): void {
|
|
441
|
+
setInlayHints(hints: readonly EditorInlayHint[], owner: string = DEFAULT_WIDGET_OWNER): void {
|
|
442
|
+
if (hints.length === 0) {
|
|
443
|
+
this.inlayOwners.delete(owner);
|
|
444
|
+
} else {
|
|
445
|
+
this.inlayOwners.set(owner, hints);
|
|
446
|
+
}
|
|
433
447
|
this.view.setInlays(
|
|
434
|
-
|
|
448
|
+
[...this.inlayOwners.values()].flat().map((hint, index) => ({
|
|
435
449
|
id: hint.id ?? `hint-${index}`,
|
|
436
450
|
at: this.offsetAt(hint.position),
|
|
437
451
|
text: hint.label,
|
|
438
452
|
...(hint.parts === undefined ? {} : { parts: hint.parts }),
|
|
439
453
|
...(hint.paddingLeft ? { gapLeft: INLAY_GAP } : {}),
|
|
440
|
-
...(hint.paddingRight ? { gapRight: INLAY_GAP } : {})
|
|
454
|
+
...(hint.paddingRight ? { gapRight: INLAY_GAP } : {}),
|
|
455
|
+
...(hint.swatch === undefined ? {} : { swatch: hint.swatch })
|
|
441
456
|
}))
|
|
442
457
|
);
|
|
443
458
|
}
|
|
@@ -938,6 +953,30 @@ class SmartEditor implements Editor {
|
|
|
938
953
|
this.view.setFoldHints(hints === null ? null : this.foldHintsByOffset(hints));
|
|
939
954
|
}
|
|
940
955
|
|
|
956
|
+
setDeclarations(positions: readonly EditorPosition[] | null): void {
|
|
957
|
+
this.view.setDeclarations(positions === null ? null : positions.map((position) => this.offsetAt(position)));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
onNavigate(listener: (from: EditorPosition) => void): () => void {
|
|
961
|
+
return this.controller.onNavigate((from) => listener(this.positionAt(from)));
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
hasFocus(): boolean {
|
|
965
|
+
return this.view.focused;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
setWordSources(source: (() => readonly string[]) | null): void {
|
|
969
|
+
this.controller.setWordSources(source);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
onCopy(listener: (copy: EditorCopy) => void): () => void {
|
|
973
|
+
return this.controller.onCopy(listener);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
paste(text: string, options?: EditorPasteOptions): boolean {
|
|
977
|
+
return this.controller.pasteText(text, options);
|
|
978
|
+
}
|
|
979
|
+
|
|
941
980
|
private foldHintsByOffset(hints: EditorFoldHints): FoldHints {
|
|
942
981
|
const lineOf = (line: number): { start: number; end: number } => this.model.getLine(Math.min(line, this.model.getLineCount() - 1));
|
|
943
982
|
|
|
@@ -1033,6 +1072,24 @@ class SmartEditor implements Editor {
|
|
|
1033
1072
|
return this.settings.readOnly;
|
|
1034
1073
|
}
|
|
1035
1074
|
|
|
1075
|
+
placeAt(position: EditorPosition): EditorTextPlace {
|
|
1076
|
+
return placeAt(this.model, this, position, this.settings.language ?? 'plaintext');
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
postfixOperands(dot: EditorPosition): readonly EditorPostfixOperand[] {
|
|
1080
|
+
return postfixOperands(this.model, this, dot, this.settings.language ?? 'plaintext');
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
surroundShape(range: EditorRange): EditorSurroundShape | null {
|
|
1084
|
+
return surroundShape(this.model, this, range, this.settings.language ?? 'plaintext');
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
unwrapTargets(position: EditorPosition): readonly EditorUnwrapTarget[] {
|
|
1088
|
+
const unit = this.settings.insertSpaces ? ' '.repeat(this.settings.tabSize) : '\t';
|
|
1089
|
+
|
|
1090
|
+
return unwrapTargets(this.model, this, position, this.settings.language ?? 'plaintext', unit);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1036
1093
|
runCommand(command: EditorRunCommand): boolean {
|
|
1037
1094
|
return this.controller.command(command);
|
|
1038
1095
|
}
|
package/src/fake.ts
CHANGED
|
@@ -34,6 +34,10 @@ import type {
|
|
|
34
34
|
EditorMessages,
|
|
35
35
|
EditorOptions,
|
|
36
36
|
EditorPosition,
|
|
37
|
+
EditorPostfixOperand,
|
|
38
|
+
EditorSurroundShape,
|
|
39
|
+
EditorTextPlace,
|
|
40
|
+
EditorUnwrapTarget,
|
|
37
41
|
EditorRemoteCursor,
|
|
38
42
|
EditorRange,
|
|
39
43
|
EditorReveal,
|
|
@@ -46,6 +50,8 @@ import type {
|
|
|
46
50
|
EditorSemanticToken,
|
|
47
51
|
EditorSmartKeys,
|
|
48
52
|
EditorApplyEditsOptions,
|
|
53
|
+
EditorCopy,
|
|
54
|
+
EditorPasteOptions,
|
|
49
55
|
EditorTextChange,
|
|
50
56
|
EditorTrackedRange,
|
|
51
57
|
EditorTheme,
|
|
@@ -53,7 +59,8 @@ import type {
|
|
|
53
59
|
EditorWidget
|
|
54
60
|
} from './types.ts';
|
|
55
61
|
import { emit, type Listener, subscribe } from './listeners.ts';
|
|
56
|
-
import { changedSpan, changedSpans } from '@adecore/editor-core';
|
|
62
|
+
import { changedSpan, changedSpans, DocumentModel } from '@adecore/editor-core';
|
|
63
|
+
import { placeAt, postfixOperands, surroundShape, unwrapTargets } from './text-queries.ts';
|
|
57
64
|
import { mapTrackedRange } from './tracked-range.ts';
|
|
58
65
|
|
|
59
66
|
interface OffsetSelection {
|
|
@@ -823,6 +830,78 @@ export class FakeEditor implements Editor {
|
|
|
823
830
|
this.foldHints = hints;
|
|
824
831
|
}
|
|
825
832
|
|
|
833
|
+
declarations: readonly EditorPosition[] | null = null;
|
|
834
|
+
|
|
835
|
+
setDeclarations(positions: readonly EditorPosition[] | null): void {
|
|
836
|
+
this.declarations = positions;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private readonly navigations = new Set<(from: EditorPosition) => void>();
|
|
840
|
+
|
|
841
|
+
onNavigate(listener: (from: EditorPosition) => void): () => void {
|
|
842
|
+
return subscribe(this.navigations, listener);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/* Stands for a command that navigated: tells the listeners the caret left `from`. */
|
|
846
|
+
navigate(from: EditorPosition): void {
|
|
847
|
+
emit(this.navigations, from);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
hasFocus(): boolean {
|
|
851
|
+
return this.focused;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private readonly copies = new Set<(copy: EditorCopy) => void>();
|
|
855
|
+
|
|
856
|
+
onCopy(listener: (copy: EditorCopy) => void): () => void {
|
|
857
|
+
return subscribe(this.copies, listener);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/* Stands for a copy or cut of a person: tells the listeners what went on the clipboard. */
|
|
861
|
+
copy(text: string, wholeLines = false, cut = false): void {
|
|
862
|
+
emit(this.copies, { text, wholeLines, cut });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/* Every paste a host asked for, with its options. */
|
|
866
|
+
readonly pastes: Array<{ text: string; options: EditorPasteOptions | undefined }> = [];
|
|
867
|
+
|
|
868
|
+
/* Puts the text at every caret, one line per caret when the lines are as many as the carets, and leaves the carets behind it. */
|
|
869
|
+
paste(text: string, options?: EditorPasteOptions): boolean {
|
|
870
|
+
this.assertLive();
|
|
871
|
+
this.pastes.push({ text, options });
|
|
872
|
+
|
|
873
|
+
if (this.readOnly || text === '') {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const lines = text.split('\n');
|
|
878
|
+
const selections = [...this.selections].sort((left, right) => Math.min(left.anchor, left.head) - Math.min(right.anchor, right.head));
|
|
879
|
+
const perCaret = lines.length === selections.length;
|
|
880
|
+
|
|
881
|
+
const edits = selections.map((selection, index) => ({
|
|
882
|
+
from: Math.min(selection.anchor, selection.head),
|
|
883
|
+
to: Math.max(selection.anchor, selection.head),
|
|
884
|
+
text: perCaret ? lines[index]! : text
|
|
885
|
+
}));
|
|
886
|
+
|
|
887
|
+
return this.edit(edits, 'command', false, () => {
|
|
888
|
+
let shift = 0;
|
|
889
|
+
|
|
890
|
+
return edits.map((edit) => {
|
|
891
|
+
const caret = edit.from + shift + edit.text.length;
|
|
892
|
+
shift += edit.text.length - (edit.to - edit.from);
|
|
893
|
+
|
|
894
|
+
return { anchor: caret, head: caret };
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
wordSources: (() => readonly string[]) | null = null;
|
|
900
|
+
|
|
901
|
+
setWordSources(source: (() => readonly string[]) | null): void {
|
|
902
|
+
this.wordSources = source;
|
|
903
|
+
}
|
|
904
|
+
|
|
826
905
|
setSmartKeys(keys: Partial<EditorSmartKeys>): void {
|
|
827
906
|
this.smartKeys = keys;
|
|
828
907
|
}
|
|
@@ -847,11 +926,17 @@ export class FakeEditor implements Editor {
|
|
|
847
926
|
this.markers = markers;
|
|
848
927
|
}
|
|
849
928
|
|
|
850
|
-
/* What the host drew as hints last. */
|
|
929
|
+
/* What the host drew as hints last, every owner's together. */
|
|
851
930
|
inlayHints: readonly EditorInlayHint[] = [];
|
|
931
|
+
private readonly inlayOwners = new Map<string, readonly EditorInlayHint[]>();
|
|
852
932
|
|
|
853
|
-
setInlayHints(hints: readonly EditorInlayHint[]): void {
|
|
854
|
-
|
|
933
|
+
setInlayHints(hints: readonly EditorInlayHint[], owner = 'default'): void {
|
|
934
|
+
if (hints.length === 0) {
|
|
935
|
+
this.inlayOwners.delete(owner);
|
|
936
|
+
} else {
|
|
937
|
+
this.inlayOwners.set(owner, hints);
|
|
938
|
+
}
|
|
939
|
+
this.inlayHints = [...this.inlayOwners.values()].flat();
|
|
855
940
|
}
|
|
856
941
|
|
|
857
942
|
private readonly inlayHovers = new Set<(target: EditorInlayTarget | null) => void>();
|
|
@@ -1190,6 +1275,24 @@ export class FakeEditor implements Editor {
|
|
|
1190
1275
|
return this.readOnly;
|
|
1191
1276
|
}
|
|
1192
1277
|
|
|
1278
|
+
placeAt(position: EditorPosition): EditorTextPlace {
|
|
1279
|
+
return placeAt(new DocumentModel(this.text), this, position, this.language ?? 'plaintext');
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
postfixOperands(dot: EditorPosition): readonly EditorPostfixOperand[] {
|
|
1283
|
+
return postfixOperands(new DocumentModel(this.text), this, dot, this.language ?? 'plaintext');
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
surroundShape(range: EditorRange): EditorSurroundShape | null {
|
|
1287
|
+
return surroundShape(new DocumentModel(this.text), this, range, this.language ?? 'plaintext');
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
unwrapTargets(position: EditorPosition): readonly EditorUnwrapTarget[] {
|
|
1291
|
+
const unit = this.indentation.insertSpaces ? ' '.repeat(this.indentation.tabSize) : '\t';
|
|
1292
|
+
|
|
1293
|
+
return unwrapTargets(new DocumentModel(this.text), this, position, this.language ?? 'plaintext', unit);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1193
1296
|
/* The commands run, in order; of its own the fake carries out only undo and redo. */
|
|
1194
1297
|
readonly commands: EditorRunCommand[] = [];
|
|
1195
1298
|
|
|
@@ -1208,6 +1311,8 @@ export class FakeEditor implements Editor {
|
|
|
1208
1311
|
this.changes.clear();
|
|
1209
1312
|
this.textChanges.clear();
|
|
1210
1313
|
this.carets.clear();
|
|
1314
|
+
this.navigations.clear();
|
|
1315
|
+
this.copies.clear();
|
|
1211
1316
|
this.hovers.clear();
|
|
1212
1317
|
this.inlayHovers.clear();
|
|
1213
1318
|
this.inlayHandlers.clear();
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,9 @@ export { AGENT_COLORS } from './attribution.ts';
|
|
|
3
3
|
export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from '@adecore/editor-core';
|
|
4
4
|
export { DEFAULT_TODO_WORDS, findTodos, type TodoMatch, type TodoOptions } from '@adecore/editor-core';
|
|
5
5
|
export { detectIndentation, type DetectedIndentation } from '@adecore/editor-core';
|
|
6
|
+
export { commentSyntax, type CommentSyntax } from '@adecore/editor-core';
|
|
6
7
|
export { createSmartEditorEngine } from './engine.ts';
|
|
8
|
+
export { rememberCopy } from './controller.ts';
|
|
7
9
|
export { DEFAULT_SMART_KEYS } from './smart-keys.ts';
|
|
8
10
|
export { shikiScopeColors, shikiTokenizers } from './shiki.ts';
|
|
9
11
|
|
|
@@ -61,6 +63,12 @@ export type {
|
|
|
61
63
|
EditorRemoteCursor,
|
|
62
64
|
EditorRange,
|
|
63
65
|
EditorApplyEditsOptions,
|
|
66
|
+
EditorCopy,
|
|
67
|
+
EditorPasteOptions,
|
|
68
|
+
EditorPostfixOperand,
|
|
69
|
+
EditorSurroundShape,
|
|
70
|
+
EditorTextPlace,
|
|
71
|
+
EditorUnwrapTarget,
|
|
64
72
|
EditorTextChange,
|
|
65
73
|
EditorTrackedRange,
|
|
66
74
|
EditorRunCommand,
|
package/src/keymap-table.ts
CHANGED
|
@@ -33,10 +33,15 @@ export const KEYMAP = {
|
|
|
33
33
|
deleteLine: bind('Mod+Backspace', 'Mod+Y'),
|
|
34
34
|
moveLineUp: bind('Alt+Shift+ArrowUp'),
|
|
35
35
|
moveLineDown: bind('Alt+Shift+ArrowDown'),
|
|
36
|
+
moveStatementUp: bind('Mod+Shift+ArrowUp'),
|
|
37
|
+
moveStatementDown: bind('Mod+Shift+ArrowDown'),
|
|
36
38
|
toggleLineComment: bind('Mod+/'),
|
|
37
39
|
toggleBlockComment: bind('Mod+Alt+/', 'Mod+Shift+/'),
|
|
38
40
|
joinLines: bind('Ctrl+Shift+J'),
|
|
39
41
|
startNewLine: bind('Shift+Enter'),
|
|
42
|
+
completeCurrentStatement: bind('Mod+Shift+Enter'),
|
|
43
|
+
hippieCompletion: bind('Alt+/'),
|
|
44
|
+
hippieBackwardCompletion: bind('Alt+Shift+/'),
|
|
40
45
|
startNewLineBefore: bind('Mod+Alt+Enter'),
|
|
41
46
|
splitLine: bind('Mod+Enter'),
|
|
42
47
|
toggleCase: bind('Mod+Shift+U'),
|
|
@@ -49,8 +54,21 @@ export const KEYMAP = {
|
|
|
49
54
|
convertIndentsToTabs: bind(null),
|
|
50
55
|
|
|
51
56
|
expandSelection: bind('Alt+ArrowUp', 'Ctrl+W'),
|
|
52
|
-
|
|
57
|
+
// Off macOS Alt+ArrowDown is Next Method, as in IntelliJ, so Shrink Selection takes its own chord there.
|
|
58
|
+
shrinkSelection: bind('Alt+ArrowDown', 'Ctrl+Shift+W'),
|
|
53
59
|
matchBrace: bind('Ctrl+M', 'Ctrl+Shift+M'),
|
|
60
|
+
methodUp: bind('Ctrl+Shift+ArrowUp', 'Alt+ArrowUp'),
|
|
61
|
+
methodDown: bind('Ctrl+Shift+ArrowDown', 'Alt+ArrowDown'),
|
|
62
|
+
lastEditLocation: bind('Mod+Shift+Backspace'),
|
|
63
|
+
nextEditLocation: bind(null),
|
|
64
|
+
pasteFromHistory: bind('Mod+Shift+V'),
|
|
65
|
+
pasteAsPlainText: bind('Mod+Alt+Shift+V'),
|
|
66
|
+
nextChange: bind('Ctrl+Alt+Shift+ArrowDown'),
|
|
67
|
+
previousChange: bind('Ctrl+Alt+Shift+ArrowUp'),
|
|
68
|
+
codeBlockStart: bind('Mod+Alt+[', 'Ctrl+['),
|
|
69
|
+
codeBlockEnd: bind('Mod+Alt+]', 'Ctrl+]'),
|
|
70
|
+
codeBlockStartWithSelection: bind('Mod+Alt+Shift+[', 'Ctrl+Shift+['),
|
|
71
|
+
codeBlockEndWithSelection: bind('Mod+Alt+Shift+]', 'Ctrl+Shift+]'),
|
|
54
72
|
|
|
55
73
|
selectNextOccurrence: bind('Ctrl+G', 'Alt+J'),
|
|
56
74
|
unselectOccurrence: bind('Ctrl+Shift+G', 'Alt+Shift+J'),
|
|
@@ -72,9 +90,10 @@ export const KEYMAP = {
|
|
|
72
90
|
smartEnd: bind('Meta+ArrowRight', null),
|
|
73
91
|
selectSmartEnd: bind('Meta+Shift+ArrowRight', null),
|
|
74
92
|
textStart: bind('Meta+ArrowUp', null),
|
|
75
|
-
|
|
93
|
+
// IntelliJ binds Cmd+Shift+ArrowUp and ArrowDown to Move Statement on macOS, where Cmd+Shift+Home and End still select to the text edges.
|
|
94
|
+
selectTextStart: bind(null),
|
|
76
95
|
textEnd: bind('Meta+ArrowDown', null),
|
|
77
|
-
selectTextEnd: bind(
|
|
96
|
+
selectTextEnd: bind(null),
|
|
78
97
|
|
|
79
98
|
collapse: bind('Mod+-'),
|
|
80
99
|
expand: bind('Mod+='),
|
|
@@ -104,6 +123,13 @@ export const KEYMAP = {
|
|
|
104
123
|
renameSymbol: bind('Shift+F6'),
|
|
105
124
|
organizeImports: bind('Ctrl+Alt+O'),
|
|
106
125
|
formatDocument: bind('Mod+Alt+L'),
|
|
126
|
+
insertLiveTemplate: bind('Mod+J'),
|
|
127
|
+
surroundWith: bind('Mod+Alt+T'),
|
|
128
|
+
surroundWithLiveTemplate: bind('Mod+Alt+J'),
|
|
129
|
+
unwrap: bind('Mod+Shift+Delete'),
|
|
130
|
+
saveAsLiveTemplate: bind(null),
|
|
131
|
+
// Surround with Emmet has no chord of its own: it is a row of Surround with Live Template, which has this one.
|
|
132
|
+
wrapWithAbbreviation: bind(null),
|
|
107
133
|
selectionToChat: bind(null),
|
|
108
134
|
inlineEdit: bind(null),
|
|
109
135
|
suggestInline: bind(null),
|
|
@@ -116,6 +142,7 @@ export const KEYMAP = {
|
|
|
116
142
|
peekDefinition: bind('Alt+Space', 'Ctrl+Shift+I'),
|
|
117
143
|
peekReferences: bind('Alt+F7'),
|
|
118
144
|
callHierarchy: bind('Ctrl+Alt+H'),
|
|
145
|
+
typeHierarchy: bind('Ctrl+H'),
|
|
119
146
|
toggleRenderedDocs: bind('Ctrl+Alt+Q'),
|
|
120
147
|
historyBack: bind('Mod+[', 'Ctrl+Alt+ArrowLeft'),
|
|
121
148
|
historyForward: bind('Mod+]', 'Ctrl+Alt+ArrowRight'),
|
|
@@ -142,7 +169,31 @@ export type OptionalKeymapId =
|
|
|
142
169
|
| 'reverseLines'
|
|
143
170
|
| 'uniqueLines'
|
|
144
171
|
| 'convertIndentsToSpaces'
|
|
145
|
-
| 'convertIndentsToTabs'
|
|
172
|
+
| 'convertIndentsToTabs'
|
|
173
|
+
| 'completeCurrentStatement'
|
|
174
|
+
| 'hippieCompletion'
|
|
175
|
+
| 'hippieBackwardCompletion'
|
|
176
|
+
| 'moveStatementUp'
|
|
177
|
+
| 'moveStatementDown'
|
|
178
|
+
| 'lastEditLocation'
|
|
179
|
+
| 'nextEditLocation'
|
|
180
|
+
| 'methodUp'
|
|
181
|
+
| 'methodDown'
|
|
182
|
+
| 'pasteFromHistory'
|
|
183
|
+
| 'pasteAsPlainText'
|
|
184
|
+
| 'nextChange'
|
|
185
|
+
| 'previousChange'
|
|
186
|
+
| 'codeBlockStart'
|
|
187
|
+
| 'codeBlockEnd'
|
|
188
|
+
| 'codeBlockStartWithSelection'
|
|
189
|
+
| 'codeBlockEndWithSelection'
|
|
190
|
+
| 'wrapWithAbbreviation'
|
|
191
|
+
| 'insertLiveTemplate'
|
|
192
|
+
| 'surroundWith'
|
|
193
|
+
| 'surroundWithLiveTemplate'
|
|
194
|
+
| 'unwrap'
|
|
195
|
+
| 'saveAsLiveTemplate'
|
|
196
|
+
| 'typeHierarchy';
|
|
146
197
|
|
|
147
198
|
export type Keymap = Readonly<Record<Exclude<KeymapId, OptionalKeymapId>, KeyBinding> & Partial<Record<OptionalKeymapId, KeyBinding>>>;
|
|
148
199
|
export type KeymapOverrides = Partial<Record<KeymapId, Partial<KeyBinding>>>;
|