@adecore/editor 0.42.0 → 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 +9 -2
- package/dist/controller.js +82 -8
- package/dist/editor.css +13 -0
- package/dist/engine.js +30 -3
- package/dist/fake.d.ts +19 -2
- package/dist/fake.js +65 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/keymap-table.d.ts +20 -1
- package/dist/keymap-table.js +24 -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/types.d.ts +18 -2
- 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 +37 -3
- package/src/fake.ts +85 -3
- package/src/index.ts +3 -0
- package/src/keymap-table.ts +44 -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/types.ts +37 -2
- package/src/view.ts +95 -9
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,
|
|
@@ -90,6 +92,8 @@ class SmartEditor implements Editor {
|
|
|
90
92
|
private lensRevisions = new Map<string, { signature: string; revision: number }>();
|
|
91
93
|
private tokenizerRequest = 0;
|
|
92
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[]>();
|
|
93
97
|
private disposed = false;
|
|
94
98
|
|
|
95
99
|
private readonly engine: SmartEditorEngineOptions;
|
|
@@ -429,15 +433,21 @@ class SmartEditor implements Editor {
|
|
|
429
433
|
);
|
|
430
434
|
}
|
|
431
435
|
|
|
432
|
-
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
|
+
}
|
|
433
442
|
this.view.setInlays(
|
|
434
|
-
|
|
443
|
+
[...this.inlayOwners.values()].flat().map((hint, index) => ({
|
|
435
444
|
id: hint.id ?? `hint-${index}`,
|
|
436
445
|
at: this.offsetAt(hint.position),
|
|
437
446
|
text: hint.label,
|
|
438
447
|
...(hint.parts === undefined ? {} : { parts: hint.parts }),
|
|
439
448
|
...(hint.paddingLeft ? { gapLeft: INLAY_GAP } : {}),
|
|
440
|
-
...(hint.paddingRight ? { gapRight: INLAY_GAP } : {})
|
|
449
|
+
...(hint.paddingRight ? { gapRight: INLAY_GAP } : {}),
|
|
450
|
+
...(hint.swatch === undefined ? {} : { swatch: hint.swatch })
|
|
441
451
|
}))
|
|
442
452
|
);
|
|
443
453
|
}
|
|
@@ -938,6 +948,30 @@ class SmartEditor implements Editor {
|
|
|
938
948
|
this.view.setFoldHints(hints === null ? null : this.foldHintsByOffset(hints));
|
|
939
949
|
}
|
|
940
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
|
+
|
|
941
975
|
private foldHintsByOffset(hints: EditorFoldHints): FoldHints {
|
|
942
976
|
const lineOf = (line: number): { start: number; end: number } => this.model.getLine(Math.min(line, this.model.getLineCount() - 1));
|
|
943
977
|
|
package/src/fake.ts
CHANGED
|
@@ -46,6 +46,8 @@ import type {
|
|
|
46
46
|
EditorSemanticToken,
|
|
47
47
|
EditorSmartKeys,
|
|
48
48
|
EditorApplyEditsOptions,
|
|
49
|
+
EditorCopy,
|
|
50
|
+
EditorPasteOptions,
|
|
49
51
|
EditorTextChange,
|
|
50
52
|
EditorTrackedRange,
|
|
51
53
|
EditorTheme,
|
|
@@ -823,6 +825,78 @@ export class FakeEditor implements Editor {
|
|
|
823
825
|
this.foldHints = hints;
|
|
824
826
|
}
|
|
825
827
|
|
|
828
|
+
declarations: readonly EditorPosition[] | null = null;
|
|
829
|
+
|
|
830
|
+
setDeclarations(positions: readonly EditorPosition[] | null): void {
|
|
831
|
+
this.declarations = positions;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private readonly navigations = new Set<(from: EditorPosition) => void>();
|
|
835
|
+
|
|
836
|
+
onNavigate(listener: (from: EditorPosition) => void): () => void {
|
|
837
|
+
return subscribe(this.navigations, listener);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/* Stands for a command that navigated: tells the listeners the caret left `from`. */
|
|
841
|
+
navigate(from: EditorPosition): void {
|
|
842
|
+
emit(this.navigations, from);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
hasFocus(): boolean {
|
|
846
|
+
return this.focused;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private readonly copies = new Set<(copy: EditorCopy) => void>();
|
|
850
|
+
|
|
851
|
+
onCopy(listener: (copy: EditorCopy) => void): () => void {
|
|
852
|
+
return subscribe(this.copies, listener);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/* Stands for a copy or cut of a person: tells the listeners what went on the clipboard. */
|
|
856
|
+
copy(text: string, wholeLines = false, cut = false): void {
|
|
857
|
+
emit(this.copies, { text, wholeLines, cut });
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/* Every paste a host asked for, with its options. */
|
|
861
|
+
readonly pastes: Array<{ text: string; options: EditorPasteOptions | undefined }> = [];
|
|
862
|
+
|
|
863
|
+
/* 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. */
|
|
864
|
+
paste(text: string, options?: EditorPasteOptions): boolean {
|
|
865
|
+
this.assertLive();
|
|
866
|
+
this.pastes.push({ text, options });
|
|
867
|
+
|
|
868
|
+
if (this.readOnly || text === '') {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const lines = text.split('\n');
|
|
873
|
+
const selections = [...this.selections].sort((left, right) => Math.min(left.anchor, left.head) - Math.min(right.anchor, right.head));
|
|
874
|
+
const perCaret = lines.length === selections.length;
|
|
875
|
+
|
|
876
|
+
const edits = selections.map((selection, index) => ({
|
|
877
|
+
from: Math.min(selection.anchor, selection.head),
|
|
878
|
+
to: Math.max(selection.anchor, selection.head),
|
|
879
|
+
text: perCaret ? lines[index]! : text
|
|
880
|
+
}));
|
|
881
|
+
|
|
882
|
+
return this.edit(edits, 'command', false, () => {
|
|
883
|
+
let shift = 0;
|
|
884
|
+
|
|
885
|
+
return edits.map((edit) => {
|
|
886
|
+
const caret = edit.from + shift + edit.text.length;
|
|
887
|
+
shift += edit.text.length - (edit.to - edit.from);
|
|
888
|
+
|
|
889
|
+
return { anchor: caret, head: caret };
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
wordSources: (() => readonly string[]) | null = null;
|
|
895
|
+
|
|
896
|
+
setWordSources(source: (() => readonly string[]) | null): void {
|
|
897
|
+
this.wordSources = source;
|
|
898
|
+
}
|
|
899
|
+
|
|
826
900
|
setSmartKeys(keys: Partial<EditorSmartKeys>): void {
|
|
827
901
|
this.smartKeys = keys;
|
|
828
902
|
}
|
|
@@ -847,11 +921,17 @@ export class FakeEditor implements Editor {
|
|
|
847
921
|
this.markers = markers;
|
|
848
922
|
}
|
|
849
923
|
|
|
850
|
-
/* What the host drew as hints last. */
|
|
924
|
+
/* What the host drew as hints last, every owner's together. */
|
|
851
925
|
inlayHints: readonly EditorInlayHint[] = [];
|
|
926
|
+
private readonly inlayOwners = new Map<string, readonly EditorInlayHint[]>();
|
|
852
927
|
|
|
853
|
-
setInlayHints(hints: readonly EditorInlayHint[]): void {
|
|
854
|
-
|
|
928
|
+
setInlayHints(hints: readonly EditorInlayHint[], owner = 'default'): void {
|
|
929
|
+
if (hints.length === 0) {
|
|
930
|
+
this.inlayOwners.delete(owner);
|
|
931
|
+
} else {
|
|
932
|
+
this.inlayOwners.set(owner, hints);
|
|
933
|
+
}
|
|
934
|
+
this.inlayHints = [...this.inlayOwners.values()].flat();
|
|
855
935
|
}
|
|
856
936
|
|
|
857
937
|
private readonly inlayHovers = new Set<(target: EditorInlayTarget | null) => void>();
|
|
@@ -1208,6 +1288,8 @@ export class FakeEditor implements Editor {
|
|
|
1208
1288
|
this.changes.clear();
|
|
1209
1289
|
this.textChanges.clear();
|
|
1210
1290
|
this.carets.clear();
|
|
1291
|
+
this.navigations.clear();
|
|
1292
|
+
this.copies.clear();
|
|
1211
1293
|
this.hovers.clear();
|
|
1212
1294
|
this.inlayHovers.clear();
|
|
1213
1295
|
this.inlayHandlers.clear();
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from '@adecore/edi
|
|
|
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
6
|
export { createSmartEditorEngine } from './engine.ts';
|
|
7
|
+
export { rememberCopy } from './controller.ts';
|
|
7
8
|
export { DEFAULT_SMART_KEYS } from './smart-keys.ts';
|
|
8
9
|
export { shikiScopeColors, shikiTokenizers } from './shiki.ts';
|
|
9
10
|
|
|
@@ -61,6 +62,8 @@ export type {
|
|
|
61
62
|
EditorRemoteCursor,
|
|
62
63
|
EditorRange,
|
|
63
64
|
EditorApplyEditsOptions,
|
|
65
|
+
EditorCopy,
|
|
66
|
+
EditorPasteOptions,
|
|
64
67
|
EditorTextChange,
|
|
65
68
|
EditorTrackedRange,
|
|
66
69
|
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,7 @@ export const KEYMAP = {
|
|
|
104
123
|
renameSymbol: bind('Shift+F6'),
|
|
105
124
|
organizeImports: bind('Ctrl+Alt+O'),
|
|
106
125
|
formatDocument: bind('Mod+Alt+L'),
|
|
126
|
+
wrapWithAbbreviation: bind('Mod+Alt+J'),
|
|
107
127
|
selectionToChat: bind(null),
|
|
108
128
|
inlineEdit: bind(null),
|
|
109
129
|
suggestInline: bind(null),
|
|
@@ -116,6 +136,7 @@ export const KEYMAP = {
|
|
|
116
136
|
peekDefinition: bind('Alt+Space', 'Ctrl+Shift+I'),
|
|
117
137
|
peekReferences: bind('Alt+F7'),
|
|
118
138
|
callHierarchy: bind('Ctrl+Alt+H'),
|
|
139
|
+
typeHierarchy: bind('Ctrl+H'),
|
|
119
140
|
toggleRenderedDocs: bind('Ctrl+Alt+Q'),
|
|
120
141
|
historyBack: bind('Mod+[', 'Ctrl+Alt+ArrowLeft'),
|
|
121
142
|
historyForward: bind('Mod+]', 'Ctrl+Alt+ArrowRight'),
|
|
@@ -142,7 +163,26 @@ export type OptionalKeymapId =
|
|
|
142
163
|
| 'reverseLines'
|
|
143
164
|
| 'uniqueLines'
|
|
144
165
|
| 'convertIndentsToSpaces'
|
|
145
|
-
| 'convertIndentsToTabs'
|
|
166
|
+
| 'convertIndentsToTabs'
|
|
167
|
+
| 'completeCurrentStatement'
|
|
168
|
+
| 'hippieCompletion'
|
|
169
|
+
| 'hippieBackwardCompletion'
|
|
170
|
+
| 'moveStatementUp'
|
|
171
|
+
| 'moveStatementDown'
|
|
172
|
+
| 'lastEditLocation'
|
|
173
|
+
| 'nextEditLocation'
|
|
174
|
+
| 'methodUp'
|
|
175
|
+
| 'methodDown'
|
|
176
|
+
| 'pasteFromHistory'
|
|
177
|
+
| 'pasteAsPlainText'
|
|
178
|
+
| 'nextChange'
|
|
179
|
+
| 'previousChange'
|
|
180
|
+
| 'codeBlockStart'
|
|
181
|
+
| 'codeBlockEnd'
|
|
182
|
+
| 'codeBlockStartWithSelection'
|
|
183
|
+
| 'codeBlockEndWithSelection'
|
|
184
|
+
| 'wrapWithAbbreviation'
|
|
185
|
+
| 'typeHierarchy';
|
|
146
186
|
|
|
147
187
|
export type Keymap = Readonly<Record<Exclude<KeymapId, OptionalKeymapId>, KeyBinding> & Partial<Record<OptionalKeymapId, KeyBinding>>>;
|
|
148
188
|
export type KeymapOverrides = Partial<Record<KeymapId, Partial<KeyBinding>>>;
|
package/src/keymap.ts
CHANGED
|
@@ -44,10 +44,15 @@ const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
|
|
|
44
44
|
deleteLine: command('deleteLine'),
|
|
45
45
|
moveLineUp: command('moveLineUp'),
|
|
46
46
|
moveLineDown: command('moveLineDown'),
|
|
47
|
+
moveStatementUp: command('moveStatementUp'),
|
|
48
|
+
moveStatementDown: command('moveStatementDown'),
|
|
47
49
|
toggleLineComment: command('toggleLineComment'),
|
|
48
50
|
toggleBlockComment: command('toggleBlockComment'),
|
|
49
51
|
joinLines: command('joinLines'),
|
|
50
52
|
startNewLine: command('startNewLine'),
|
|
53
|
+
completeCurrentStatement: command('completeCurrentStatement'),
|
|
54
|
+
hippieCompletion: command('hippieCompletion'),
|
|
55
|
+
hippieBackwardCompletion: command('hippieBackwardCompletion'),
|
|
51
56
|
startNewLineBefore: command('startNewLineBefore'),
|
|
52
57
|
splitLine: command('splitLine'),
|
|
53
58
|
toggleCase: command('toggleCase'),
|
|
@@ -60,6 +65,15 @@ const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
|
|
|
60
65
|
expandSelection: command('expandSelection'),
|
|
61
66
|
shrinkSelection: command('shrinkSelection'),
|
|
62
67
|
matchBrace: { type: 'bracket' },
|
|
68
|
+
methodUp: view('methodUp'),
|
|
69
|
+
methodDown: view('methodDown'),
|
|
70
|
+
pasteAsPlainText: view('pasteAsPlainText'),
|
|
71
|
+
nextChange: view('nextChange'),
|
|
72
|
+
previousChange: view('previousChange'),
|
|
73
|
+
codeBlockStart: command('codeBlockStart'),
|
|
74
|
+
codeBlockEnd: command('codeBlockEnd'),
|
|
75
|
+
codeBlockStartWithSelection: command('codeBlockStartWithSelection'),
|
|
76
|
+
codeBlockEndWithSelection: command('codeBlockEndWithSelection'),
|
|
63
77
|
selectNextOccurrence: command('selectNextOccurrence'),
|
|
64
78
|
unselectOccurrence: command('unselectOccurrence'),
|
|
65
79
|
selectAllOccurrences: command('selectAllOccurrences'),
|
package/src/layout.ts
CHANGED
|
@@ -57,8 +57,13 @@ export interface Inlay {
|
|
|
57
57
|
composition?: boolean;
|
|
58
58
|
/* A suggestion the host drew after the caret: text in the code face with no padding, which the caret stays in front of. */
|
|
59
59
|
ghost?: boolean;
|
|
60
|
+
/* A square of this CSS color in place of the text, such as the color a literal in the code stands for. */
|
|
61
|
+
swatch?: string;
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/* The side of a swatch in pixels, without its gaps. */
|
|
65
|
+
export const SWATCH_SIZE = 12;
|
|
66
|
+
|
|
62
67
|
/* A row of the host's own DOM above or below a line, as tall as the host makes it. */
|
|
63
68
|
export interface BlockWidget {
|
|
64
69
|
id: string;
|
|
@@ -366,10 +371,13 @@ export function scanDenseLine(
|
|
|
366
371
|
const inlay = lineInlays[nextInlay++]!;
|
|
367
372
|
const shown = visibleBidi(inlay.text);
|
|
368
373
|
|
|
374
|
+
const gaps = (inlay.gapLeft ?? 0) + (inlay.gapRight ?? 0);
|
|
369
375
|
const width =
|
|
370
376
|
inlay.composition || inlay.ghost
|
|
371
377
|
? metrics.measureText(shown)
|
|
372
|
-
:
|
|
378
|
+
: inlay.swatch !== undefined
|
|
379
|
+
? SWATCH_SIZE + gaps
|
|
380
|
+
: Math.max(8, metrics.measureInlay(shown) + 12) + gaps;
|
|
373
381
|
|
|
374
382
|
geometry.inlays.push({ inlay, x, width, subRow: row });
|
|
375
383
|
x += width;
|
package/src/outline.ts
CHANGED
|
@@ -215,6 +215,19 @@ export class Outline {
|
|
|
215
215
|
this.version++;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/* Where the named blocks of the structure declare their name, in no order: the stops of Method Up and Down. */
|
|
219
|
+
declarationStops(document: OutlineDocument): number[] {
|
|
220
|
+
return this.structure.flatMap((entry) => {
|
|
221
|
+
if (entry.name === undefined) {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const header = document.getLine(document.positionAt(entry.from).line);
|
|
226
|
+
|
|
227
|
+
return [header.start + Math.max(0, header.text.indexOf(entry.name))];
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
218
231
|
/* Outermost first. Of two blocks that start on one line, only the larger one. */
|
|
219
232
|
blocks(document: OutlineDocument): readonly OutlineBlock[] {
|
|
220
233
|
if (this.cache?.version !== this.version) {
|
package/src/paint.ts
CHANGED
|
@@ -186,11 +186,20 @@ export function lineElement(
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
const span = document.createElement('span');
|
|
189
|
-
span.className = box.inlay.ghost
|
|
189
|
+
span.className = box.inlay.ghost
|
|
190
|
+
? 'se-ghost'
|
|
191
|
+
: box.inlay.composition
|
|
192
|
+
? 'se-composition'
|
|
193
|
+
: box.inlay.swatch !== undefined
|
|
194
|
+
? 'se-inlay se-swatch'
|
|
195
|
+
: 'se-inlay';
|
|
190
196
|
span.dataset.inlayId = box.inlay.id;
|
|
191
197
|
const { parts } = box.inlay;
|
|
192
198
|
|
|
193
|
-
if (
|
|
199
|
+
if (box.inlay.swatch !== undefined) {
|
|
200
|
+
span.style.backgroundColor = box.inlay.swatch;
|
|
201
|
+
span.setAttribute('aria-label', box.inlay.text);
|
|
202
|
+
} else if (parts === undefined) {
|
|
194
203
|
span.textContent = visibleBidi(box.inlay.text);
|
|
195
204
|
} else {
|
|
196
205
|
parts.forEach((part, index) => {
|
package/src/types.ts
CHANGED
|
@@ -28,7 +28,12 @@ export type EditorViewCommand =
|
|
|
28
28
|
| 'toggleColumnMode'
|
|
29
29
|
| 'increaseFontSize'
|
|
30
30
|
| 'decreaseFontSize'
|
|
31
|
-
| 'resetFontSize'
|
|
31
|
+
| 'resetFontSize'
|
|
32
|
+
| 'methodUp'
|
|
33
|
+
| 'methodDown'
|
|
34
|
+
| 'nextChange'
|
|
35
|
+
| 'previousChange'
|
|
36
|
+
| 'pasteAsPlainText';
|
|
32
37
|
|
|
33
38
|
/* How far a person may zoom the text, and whether the wheel does. */
|
|
34
39
|
export interface EditorZoom {
|
|
@@ -283,6 +288,21 @@ export interface EditorContentChange {
|
|
|
283
288
|
readonly text: string;
|
|
284
289
|
}
|
|
285
290
|
|
|
291
|
+
/* What a copy or cut put on the clipboard. */
|
|
292
|
+
export interface EditorCopy {
|
|
293
|
+
readonly text: string;
|
|
294
|
+
/* A copy of bare carets takes their lines, which paste back above the lines of bare carets. */
|
|
295
|
+
readonly wholeLines: boolean;
|
|
296
|
+
readonly cut: boolean;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface EditorPasteOptions {
|
|
300
|
+
/* Pastes above the lines of bare carets, as the paste of a copy of bare carets does; by default when the text is the last such copy. */
|
|
301
|
+
readonly wholeLines?: boolean;
|
|
302
|
+
/* False leaves the indentation of the text alone; the editor's indent on paste setting otherwise. */
|
|
303
|
+
readonly reindent?: boolean;
|
|
304
|
+
}
|
|
305
|
+
|
|
286
306
|
export interface EditorApplyEditsOptions {
|
|
287
307
|
readonly mapSelections?: (range: EditorRange) => EditorRange;
|
|
288
308
|
readonly mergeUndo?: boolean;
|
|
@@ -472,6 +492,8 @@ export interface EditorInlayHint {
|
|
|
472
492
|
/* A gap of 4 pixels that keeps the pill off the character before it or the one after it. */
|
|
473
493
|
readonly paddingLeft?: boolean;
|
|
474
494
|
readonly paddingRight?: boolean;
|
|
495
|
+
/* A square of this CSS color in place of the label, such as the color a literal stands for; the label is then what assistive technology reads. */
|
|
496
|
+
readonly swatch?: string;
|
|
475
497
|
}
|
|
476
498
|
|
|
477
499
|
/* A hint, or one part of its label, under the pointer. */
|
|
@@ -649,6 +671,18 @@ export interface Editor {
|
|
|
649
671
|
setLabel(label: string): void;
|
|
650
672
|
/* What a language server knows about the folds: the bodies of symbols and the ranges it folds. They follow their text through edits until set again; null forgets them. */
|
|
651
673
|
setFoldHints(hints: EditorFoldHints | null): void;
|
|
674
|
+
/* The places Method Up and Down stop at, such as the names of a language server's symbols. They follow their text through edits until set again; null goes back to the editor's own reading of the structure. Optional so an editor from before it still fits. */
|
|
675
|
+
setDeclarations?(positions: readonly EditorPosition[] | null): void;
|
|
676
|
+
/* A command that navigates (Method Up and Down, Code Block Start and End) moved the primary caret away from `from`. Optional so an editor from before it still fits. */
|
|
677
|
+
onNavigate?(listener: (from: EditorPosition) => void): () => void;
|
|
678
|
+
/* Whether the editor has the keyboard focus. Optional so an editor from before it still fits. */
|
|
679
|
+
hasFocus?(): boolean;
|
|
680
|
+
/* The texts of the other open documents, which Hippie completion takes words from after those of this one; null takes them away. Optional so an editor from before it still fits. */
|
|
681
|
+
setWordSources?(source: (() => readonly string[]) | null): void;
|
|
682
|
+
/* Told after a copy or cut, with the text it put on the clipboard. Optional so an editor from before it still fits. */
|
|
683
|
+
onCopy?(listener: (copy: EditorCopy) => void): () => void;
|
|
684
|
+
/* Pastes `text` as a paste from the clipboard does: at every caret, with indent on paste, and refused with its notice when the editor is read-only. False when nothing was pasted. Optional so an editor from before it still fits. */
|
|
685
|
+
paste?(text: string, options?: EditorPasteOptions): boolean;
|
|
652
686
|
/*
|
|
653
687
|
* Marks that follow their lines through edits until the host sets them again. Every layer (default: the unnamed one)
|
|
654
688
|
* holds its own marks and the editor draws the union, so setting one layer never clears another; an empty list
|
|
@@ -699,7 +733,8 @@ export interface Editor {
|
|
|
699
733
|
/* Colors by what the language servers know, over what the grammar made of the text; null takes them away. */
|
|
700
734
|
setSemanticTokens(tokens: readonly EditorSemanticToken[] | null): void;
|
|
701
735
|
/* Hints to draw as soft pills in the text, replacing the ones set before. They follow their text through edits until set again. */
|
|
702
|
-
|
|
736
|
+
/* Each owner sets its own hints and keeps those of the others; ids stay unique across owners. */
|
|
737
|
+
setInlayHints(hints: readonly EditorInlayHint[], owner?: string): void;
|
|
703
738
|
/* The pointer moved onto a hint or one of its parts, or null when it left them. Optional so an editor from before it still fits. */
|
|
704
739
|
onInlayHover?(listener: (target: EditorInlayTarget | null) => void): () => void;
|
|
705
740
|
/* Handlers see a press of the primary button on a hint before the editor does, the first to take it winning. */
|
package/src/view.ts
CHANGED
|
@@ -298,6 +298,7 @@ export class EditorView {
|
|
|
298
298
|
private lenses: BlockWidget[] = [];
|
|
299
299
|
private readonly replacements = new Map<string, BlockReplacement[]>();
|
|
300
300
|
private occurrences: Occurrence[] = [];
|
|
301
|
+
private hippieSource: { from: number; to: number } | null = null;
|
|
301
302
|
private replacePreview: { text: string; preserveCase: boolean } | null = null;
|
|
302
303
|
private dropCaret: number | null = null;
|
|
303
304
|
private activeGuideCache: { key: string; guide: { column: number; first: number; last: number } | null } | undefined;
|
|
@@ -316,6 +317,7 @@ export class EditorView {
|
|
|
316
317
|
private collapsed = new Set<number>();
|
|
317
318
|
/* What a language server said about the folds, in offsets that follow the text through edits. */
|
|
318
319
|
private foldHints: FoldHints | null = null;
|
|
320
|
+
private declarations: number[] | null = null;
|
|
319
321
|
/* The roles that fold by themselves while the file is as it was opened; null once a person edited or the file opened with folds of its own. */
|
|
320
322
|
private defaultRoles: ReadonlySet<FoldRole> | null = null;
|
|
321
323
|
/* The folds somebody already decided on, a person or the defaults, so a later answer of a server leaves them as they are. */
|
|
@@ -600,6 +602,16 @@ export class EditorView {
|
|
|
600
602
|
this.render();
|
|
601
603
|
}
|
|
602
604
|
|
|
605
|
+
/* The word Hippie completion took from this document, shown until the next key or edit; null takes the mark away. */
|
|
606
|
+
setHippieSource(range: { from: number; to: number } | null): void {
|
|
607
|
+
if (range === null && this.hippieSource === null) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
this.hippieSource = range;
|
|
612
|
+
this.render();
|
|
613
|
+
}
|
|
614
|
+
|
|
603
615
|
/* A name underlined as a link a press follows, with the pointer to match; null takes it away. */
|
|
604
616
|
setLink(link: { from: number; to: number } | null): void {
|
|
605
617
|
this.link = link;
|
|
@@ -849,6 +861,7 @@ export class EditorView {
|
|
|
849
861
|
this.markerVersion++;
|
|
850
862
|
const anchor = this.topAnchor;
|
|
851
863
|
this.occurrences = [];
|
|
864
|
+
this.hippieSource = null;
|
|
852
865
|
this.link = null;
|
|
853
866
|
this.content.classList.remove('se-content-link');
|
|
854
867
|
this.outline.edited(batches);
|
|
@@ -916,6 +929,10 @@ export class EditorView {
|
|
|
916
929
|
this.foldHints = mapFoldHints(this.foldHints, map);
|
|
917
930
|
}
|
|
918
931
|
|
|
932
|
+
if (this.declarations !== null) {
|
|
933
|
+
this.declarations = this.declarations.map((offset) => map(offset));
|
|
934
|
+
}
|
|
935
|
+
|
|
919
936
|
this.customFolds = this.customFolds.map((fold) => mapRange(fold, map));
|
|
920
937
|
this.find.mapBounds(changes);
|
|
921
938
|
|
|
@@ -1089,15 +1106,7 @@ export class EditorView {
|
|
|
1089
1106
|
return;
|
|
1090
1107
|
}
|
|
1091
1108
|
|
|
1092
|
-
const ranges =
|
|
1093
|
-
this.model.getLength() > FOLD_LIMIT
|
|
1094
|
-
? []
|
|
1095
|
-
: this.model.getFoldingRanges({
|
|
1096
|
-
indentation: /^(python|py|yaml|yml)$/.test(this.settings.language ?? ''),
|
|
1097
|
-
tabSize: this.settings.tabSize,
|
|
1098
|
-
...(this.settings.language === undefined ? {} : { language: this.settings.language }),
|
|
1099
|
-
...(this.foldHints === null ? {} : { hints: this.foldHints })
|
|
1100
|
-
});
|
|
1109
|
+
const ranges = this.structureRanges();
|
|
1101
1110
|
|
|
1102
1111
|
this.foldRanges = this.withCustomFolds(ranges);
|
|
1103
1112
|
|
|
@@ -1115,6 +1124,20 @@ export class EditorView {
|
|
|
1115
1124
|
this.refoldLayout();
|
|
1116
1125
|
}
|
|
1117
1126
|
|
|
1127
|
+
/* The folding ranges the document has now, a server's included, whether or not the folds are drawn. */
|
|
1128
|
+
structureRanges(): FoldingRange[] {
|
|
1129
|
+
if (this.model.getLength() > FOLD_LIMIT) {
|
|
1130
|
+
return [];
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return this.model.getFoldingRanges({
|
|
1134
|
+
indentation: /^(python|py|yaml|yml)$/.test(this.settings.language ?? ''),
|
|
1135
|
+
tabSize: this.settings.tabSize,
|
|
1136
|
+
...(this.settings.language === undefined ? {} : { language: this.settings.language }),
|
|
1137
|
+
...(this.foldHints === null ? {} : { hints: this.foldHints })
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1118
1141
|
/* The folds a person made of a selection, which the document's own structure does not know, among the ones it does. */
|
|
1119
1142
|
private withCustomFolds(ranges: readonly FoldingRange[]): ViewFold[] {
|
|
1120
1143
|
const custom = this.customFolds.flatMap((fold): ViewFold[] => {
|
|
@@ -1383,6 +1406,65 @@ export class EditorView {
|
|
|
1383
1406
|
this.refoldLayout();
|
|
1384
1407
|
}
|
|
1385
1408
|
|
|
1409
|
+
/* The places Method Up and Down stop at; they follow their text through edits until set again, and null goes back to the editor's own reading. */
|
|
1410
|
+
setDeclarations(offsets: readonly number[] | null): void {
|
|
1411
|
+
this.declarations = offsets === null ? null : [...new Set(offsets)].sort((left, right) => left - right);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
private methodStops(): number[] {
|
|
1415
|
+
if (this.declarations !== null) {
|
|
1416
|
+
return this.declarations;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const sections = this.foldRanges.filter((range) => range.kind === 'section').map((range) => this.model.getLine(range.startLine).start);
|
|
1420
|
+
|
|
1421
|
+
return [...new Set([...this.outline.declarationStops(this.model), ...sections])].sort((left, right) => left - right);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/* Moves the one caret to the first line of the next or previous change mark, of any layer. False when there is none. */
|
|
1425
|
+
goToChange(direction: -1 | 1): boolean {
|
|
1426
|
+
const line = this.model.positionAt(this.model.getPrimary().head).line;
|
|
1427
|
+
const { runs } = this.resolvedChanges();
|
|
1428
|
+
|
|
1429
|
+
// A run the caret is in is where it already is; a deleted run is the line its mark sits on.
|
|
1430
|
+
const target = direction === 1 ? runs.find((run) => run.start > line) : runs.findLast((run) => (run.kind === 'deleted' ? run.start : run.end) < line);
|
|
1431
|
+
|
|
1432
|
+
if (target === undefined) {
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const offset = this.model.getLine(Math.min(target.start, this.model.getLineCount() - 1)).start;
|
|
1437
|
+
this.model.setSelections([{ anchor: offset, head: offset }]);
|
|
1438
|
+
this.revealOffset(offset, 'center');
|
|
1439
|
+
|
|
1440
|
+
return true;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/* Moves the one caret to the previous or next declaration on another line. False when there is none. */
|
|
1444
|
+
goToMethod(direction: -1 | 1): boolean {
|
|
1445
|
+
// The structure is read after a pause in typing, so a press right after an edit reads it now.
|
|
1446
|
+
if (this.declarations === null) {
|
|
1447
|
+
this.refreshFolds();
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const line = this.model.positionAt(this.model.getPrimary().head).line;
|
|
1451
|
+
const stops = this.methodStops();
|
|
1452
|
+
|
|
1453
|
+
const target =
|
|
1454
|
+
direction === 1
|
|
1455
|
+
? stops.find((offset) => this.model.positionAt(offset).line > line)
|
|
1456
|
+
: stops.findLast((offset) => this.model.positionAt(offset).line < line);
|
|
1457
|
+
|
|
1458
|
+
if (target === undefined) {
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
this.model.setSelections([{ anchor: target, head: target }]);
|
|
1463
|
+
this.revealOffset(target, direction === 1 ? 'centerDown' : 'centerUp');
|
|
1464
|
+
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1386
1468
|
/* Hands over what a language server knows about the folds; null forgets it. The defaults fold what the answer names, once. */
|
|
1387
1469
|
setFoldHints(hints: FoldHints | null): void {
|
|
1388
1470
|
this.foldHints = hints;
|
|
@@ -2480,6 +2562,10 @@ export class EditorView {
|
|
|
2480
2562
|
}
|
|
2481
2563
|
}
|
|
2482
2564
|
|
|
2565
|
+
if (this.hippieSource !== null) {
|
|
2566
|
+
marks.push({ className: 'se-hippie-source', rects: this.paintRectangles(this.hippieSource.from, this.hippieSource.to, rows) });
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2483
2569
|
const marginX = this.settings.rightMargin === null ? null : this.settings.rightMargin * this.layout.metrics.charWidth;
|
|
2484
2570
|
|
|
2485
2571
|
// A margin past the content would stretch the scroll extent to itself, so it shows only where the text or the view reaches it.
|