@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/fake.ts
CHANGED
|
@@ -46,9 +46,12 @@ import type {
|
|
|
46
46
|
EditorSemanticToken,
|
|
47
47
|
EditorSmartKeys,
|
|
48
48
|
EditorApplyEditsOptions,
|
|
49
|
+
EditorCopy,
|
|
50
|
+
EditorPasteOptions,
|
|
49
51
|
EditorTextChange,
|
|
50
52
|
EditorTrackedRange,
|
|
51
53
|
EditorTheme,
|
|
54
|
+
EditorWhitespaceCleanup,
|
|
52
55
|
EditorWidget
|
|
53
56
|
} from './types.ts';
|
|
54
57
|
import { emit, type Listener, subscribe } from './listeners.ts';
|
|
@@ -254,6 +257,8 @@ export class FakeEditor implements Editor {
|
|
|
254
257
|
this.readOnly = options.readOnly ?? false;
|
|
255
258
|
this.readOnlyReason = options.readOnlyReason;
|
|
256
259
|
this.label = options.label ?? 'Code editor';
|
|
260
|
+
this.todoWords = options.todoWords;
|
|
261
|
+
this.fontSize = options.fontSize ?? null;
|
|
257
262
|
this.revealedLine = options.line ?? null;
|
|
258
263
|
this.column = options.column ?? null;
|
|
259
264
|
this.scrollTop = options.scrollTop ?? null;
|
|
@@ -810,10 +815,88 @@ export class FakeEditor implements Editor {
|
|
|
810
815
|
this.label = label;
|
|
811
816
|
}
|
|
812
817
|
|
|
818
|
+
todoWords: readonly string[] | undefined;
|
|
819
|
+
|
|
820
|
+
setTodoWords(words: readonly string[]): void {
|
|
821
|
+
this.todoWords = [...words];
|
|
822
|
+
}
|
|
823
|
+
|
|
813
824
|
setFoldHints(hints: EditorFoldHints | null): void {
|
|
814
825
|
this.foldHints = hints;
|
|
815
826
|
}
|
|
816
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
|
+
|
|
817
900
|
setSmartKeys(keys: Partial<EditorSmartKeys>): void {
|
|
818
901
|
this.smartKeys = keys;
|
|
819
902
|
}
|
|
@@ -838,11 +921,17 @@ export class FakeEditor implements Editor {
|
|
|
838
921
|
this.markers = markers;
|
|
839
922
|
}
|
|
840
923
|
|
|
841
|
-
/* What the host drew as hints last. */
|
|
924
|
+
/* What the host drew as hints last, every owner's together. */
|
|
842
925
|
inlayHints: readonly EditorInlayHint[] = [];
|
|
926
|
+
private readonly inlayOwners = new Map<string, readonly EditorInlayHint[]>();
|
|
843
927
|
|
|
844
|
-
setInlayHints(hints: readonly EditorInlayHint[]): void {
|
|
845
|
-
|
|
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();
|
|
846
935
|
}
|
|
847
936
|
|
|
848
937
|
private readonly inlayHovers = new Set<(target: EditorInlayTarget | null) => void>();
|
|
@@ -1126,6 +1215,22 @@ export class FakeEditor implements Editor {
|
|
|
1126
1215
|
return this.edit(ordered, 'command', options.mergeUndo === true, place);
|
|
1127
1216
|
}
|
|
1128
1217
|
|
|
1218
|
+
/* The options of every cleanup asked for, in order; the fake changes no text for them. */
|
|
1219
|
+
readonly cleanups: EditorWhitespaceCleanup[] = [];
|
|
1220
|
+
/* How often the text was marked as saved, by a cleanup or on its own. */
|
|
1221
|
+
savedMarks = 0;
|
|
1222
|
+
|
|
1223
|
+
cleanUpWhitespace(options: EditorWhitespaceCleanup): boolean {
|
|
1224
|
+
this.cleanups.push({ ...options });
|
|
1225
|
+
this.markSaved();
|
|
1226
|
+
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
markSaved(): void {
|
|
1231
|
+
this.savedMarks += 1;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1129
1234
|
setTheme(theme: EditorTheme): void {
|
|
1130
1235
|
this.theme = theme;
|
|
1131
1236
|
}
|
|
@@ -1134,6 +1239,28 @@ export class FakeEditor implements Editor {
|
|
|
1134
1239
|
this.fontRefreshes += 1;
|
|
1135
1240
|
}
|
|
1136
1241
|
|
|
1242
|
+
/* The size `setFontSize` set, null for the page's. */
|
|
1243
|
+
fontSize: number | null = null;
|
|
1244
|
+
private readonly fontSizeListeners = new Set<(size: number | null) => void>();
|
|
1245
|
+
|
|
1246
|
+
setFontSize(size: number | null): void {
|
|
1247
|
+
this.fontSize = size;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
getFontSize(): number {
|
|
1251
|
+
return this.fontSize ?? 13;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
onFontSizeChange(listener: (size: number | null) => void): () => void {
|
|
1255
|
+
return subscribe(this.fontSizeListeners, listener);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/* What a person zooming would do: the new size goes to the listeners. */
|
|
1259
|
+
zoomTo(size: number | null): void {
|
|
1260
|
+
this.fontSize = size;
|
|
1261
|
+
emit(this.fontSizeListeners, size);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1137
1264
|
setReadOnly(readOnly: boolean, reason?: string): void {
|
|
1138
1265
|
this.readOnly = readOnly;
|
|
1139
1266
|
this.readOnlyReason = reason;
|
|
@@ -1161,6 +1288,8 @@ export class FakeEditor implements Editor {
|
|
|
1161
1288
|
this.changes.clear();
|
|
1162
1289
|
this.textChanges.clear();
|
|
1163
1290
|
this.carets.clear();
|
|
1291
|
+
this.navigations.clear();
|
|
1292
|
+
this.copies.clear();
|
|
1164
1293
|
this.hovers.clear();
|
|
1165
1294
|
this.inlayHovers.clear();
|
|
1166
1295
|
this.inlayHandlers.clear();
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { MAX_REPLACEMENT_PAGE, MAX_REPLACEMENT_CHARACTERS } from './search-protocol.ts';
|
|
2
2
|
export { AGENT_COLORS } from './attribution.ts';
|
|
3
3
|
export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from '@adecore/editor-core';
|
|
4
|
+
export { DEFAULT_TODO_WORDS, findTodos, type TodoMatch, type TodoOptions } from '@adecore/editor-core';
|
|
5
|
+
export { detectIndentation, type DetectedIndentation } from '@adecore/editor-core';
|
|
4
6
|
export { createSmartEditorEngine } from './engine.ts';
|
|
7
|
+
export { rememberCopy } from './controller.ts';
|
|
5
8
|
export { DEFAULT_SMART_KEYS } from './smart-keys.ts';
|
|
6
9
|
export { shikiScopeColors, shikiTokenizers } from './shiki.ts';
|
|
7
10
|
|
|
@@ -59,13 +62,17 @@ export type {
|
|
|
59
62
|
EditorRemoteCursor,
|
|
60
63
|
EditorRange,
|
|
61
64
|
EditorApplyEditsOptions,
|
|
65
|
+
EditorCopy,
|
|
66
|
+
EditorPasteOptions,
|
|
62
67
|
EditorTextChange,
|
|
63
68
|
EditorTrackedRange,
|
|
64
69
|
EditorRunCommand,
|
|
65
70
|
EditorSelectionRanges,
|
|
66
71
|
EditorTheme,
|
|
67
72
|
EditorViewCommand,
|
|
73
|
+
EditorWhitespaceCleanup,
|
|
68
74
|
EditorWidget,
|
|
75
|
+
EditorZoom,
|
|
69
76
|
FoldRole,
|
|
70
77
|
KeyChord,
|
|
71
78
|
LineToken,
|
package/src/keymap-table.ts
CHANGED
|
@@ -33,18 +33,42 @@ 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'),
|
|
43
48
|
autoIndentLines: bind('Ctrl+Alt+I'),
|
|
49
|
+
// The platform binds no chord to these, so they live in the menu and the palette.
|
|
50
|
+
sortLines: bind(null),
|
|
51
|
+
reverseLines: bind(null),
|
|
52
|
+
uniqueLines: bind(null),
|
|
53
|
+
convertIndentsToSpaces: bind(null),
|
|
54
|
+
convertIndentsToTabs: bind(null),
|
|
44
55
|
|
|
45
56
|
expandSelection: bind('Alt+ArrowUp', 'Ctrl+W'),
|
|
46
|
-
|
|
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'),
|
|
47
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+]'),
|
|
48
72
|
|
|
49
73
|
selectNextOccurrence: bind('Ctrl+G', 'Alt+J'),
|
|
50
74
|
unselectOccurrence: bind('Ctrl+Shift+G', 'Alt+Shift+J'),
|
|
@@ -66,9 +90,10 @@ export const KEYMAP = {
|
|
|
66
90
|
smartEnd: bind('Meta+ArrowRight', null),
|
|
67
91
|
selectSmartEnd: bind('Meta+Shift+ArrowRight', null),
|
|
68
92
|
textStart: bind('Meta+ArrowUp', null),
|
|
69
|
-
|
|
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),
|
|
70
95
|
textEnd: bind('Meta+ArrowDown', null),
|
|
71
|
-
selectTextEnd: bind(
|
|
96
|
+
selectTextEnd: bind(null),
|
|
72
97
|
|
|
73
98
|
collapse: bind('Mod+-'),
|
|
74
99
|
expand: bind('Mod+='),
|
|
@@ -85,6 +110,10 @@ export const KEYMAP = {
|
|
|
85
110
|
expandAllToLevel3: bind(null),
|
|
86
111
|
expandAllToLevel4: bind(null),
|
|
87
112
|
expandAllToLevel5: bind(null),
|
|
113
|
+
// The platform binds no chord to these, and the zoom chords of other editors are the fold keys here.
|
|
114
|
+
increaseFontSize: bind(null),
|
|
115
|
+
decreaseFontSize: bind(null),
|
|
116
|
+
resetFontSize: bind(null),
|
|
88
117
|
|
|
89
118
|
triggerCompletion: bind('Ctrl+Space'),
|
|
90
119
|
parameterInfo: bind('Mod+P'),
|
|
@@ -94,6 +123,7 @@ export const KEYMAP = {
|
|
|
94
123
|
renameSymbol: bind('Shift+F6'),
|
|
95
124
|
organizeImports: bind('Ctrl+Alt+O'),
|
|
96
125
|
formatDocument: bind('Mod+Alt+L'),
|
|
126
|
+
wrapWithAbbreviation: bind('Mod+Alt+J'),
|
|
97
127
|
selectionToChat: bind(null),
|
|
98
128
|
inlineEdit: bind(null),
|
|
99
129
|
suggestInline: bind(null),
|
|
@@ -106,6 +136,7 @@ export const KEYMAP = {
|
|
|
106
136
|
peekDefinition: bind('Alt+Space', 'Ctrl+Shift+I'),
|
|
107
137
|
peekReferences: bind('Alt+F7'),
|
|
108
138
|
callHierarchy: bind('Ctrl+Alt+H'),
|
|
139
|
+
typeHierarchy: bind('Ctrl+H'),
|
|
109
140
|
toggleRenderedDocs: bind('Ctrl+Alt+Q'),
|
|
110
141
|
historyBack: bind('Mod+[', 'Ctrl+Alt+ArrowLeft'),
|
|
111
142
|
historyForward: bind('Mod+]', 'Ctrl+Alt+ArrowRight'),
|
|
@@ -123,7 +154,35 @@ export const KEYMAP = {
|
|
|
123
154
|
export type KeymapId = keyof typeof KEYMAP;
|
|
124
155
|
|
|
125
156
|
/* Commands added after a host may have written a full keymap of its own; a keymap without one falls back to the built-in chord. */
|
|
126
|
-
export type OptionalKeymapId =
|
|
157
|
+
export type OptionalKeymapId =
|
|
158
|
+
| 'toggleRenderedDocs'
|
|
159
|
+
| 'increaseFontSize'
|
|
160
|
+
| 'decreaseFontSize'
|
|
161
|
+
| 'resetFontSize'
|
|
162
|
+
| 'sortLines'
|
|
163
|
+
| 'reverseLines'
|
|
164
|
+
| 'uniqueLines'
|
|
165
|
+
| 'convertIndentsToSpaces'
|
|
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';
|
|
127
186
|
|
|
128
187
|
export type Keymap = Readonly<Record<Exclude<KeymapId, OptionalKeymapId>, KeyBinding> & Partial<Record<OptionalKeymapId, KeyBinding>>>;
|
|
129
188
|
export type KeymapOverrides = Partial<Record<KeymapId, Partial<KeyBinding>>>;
|
package/src/keymap.ts
CHANGED
|
@@ -44,17 +44,36 @@ 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'),
|
|
54
59
|
autoIndentLines: command('autoIndentLines'),
|
|
60
|
+
sortLines: command('sortLines'),
|
|
61
|
+
reverseLines: command('reverseLines'),
|
|
62
|
+
uniqueLines: command('uniqueLines'),
|
|
63
|
+
convertIndentsToSpaces: command('convertIndentsToSpaces'),
|
|
64
|
+
convertIndentsToTabs: command('convertIndentsToTabs'),
|
|
55
65
|
expandSelection: command('expandSelection'),
|
|
56
66
|
shrinkSelection: command('shrinkSelection'),
|
|
57
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'),
|
|
58
77
|
selectNextOccurrence: command('selectNextOccurrence'),
|
|
59
78
|
unselectOccurrence: command('unselectOccurrence'),
|
|
60
79
|
selectAllOccurrences: command('selectAllOccurrences'),
|
|
@@ -87,7 +106,10 @@ const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
|
|
|
87
106
|
expandAllToLevel3: view('expandAllToLevel3'),
|
|
88
107
|
expandAllToLevel4: view('expandAllToLevel4'),
|
|
89
108
|
expandAllToLevel5: view('expandAllToLevel5'),
|
|
90
|
-
toggleColumnMode: view('toggleColumnMode')
|
|
109
|
+
toggleColumnMode: view('toggleColumnMode'),
|
|
110
|
+
increaseFontSize: view('increaseFontSize'),
|
|
111
|
+
decreaseFontSize: view('decreaseFontSize'),
|
|
112
|
+
resetFontSize: view('resetFontSize')
|
|
91
113
|
};
|
|
92
114
|
|
|
93
115
|
const PUNCTUATION_CODES: Readonly<Record<string, string>> = {
|
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/overview.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type OverviewKind = 'added' | 'modified' | 'deleted' | 'find' | 'find-current' | 'occurrence' | 'info' | 'warning' | 'error';
|
|
1
|
+
export type OverviewKind = 'added' | 'modified' | 'deleted' | 'find' | 'find-current' | 'occurrence' | 'todo' | 'info' | 'warning' | 'error';
|
|
2
2
|
|
|
3
3
|
/* A stretch of the content, in the pixels the content is laid out in. */
|
|
4
4
|
export interface OverviewSpan {
|
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/todos.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { DEFAULT_TODO_WORDS, type DocumentModel, type TodoMatch } from '@adecore/editor-core';
|
|
2
|
+
import { overlayTokens } from './semantic.ts';
|
|
3
|
+
import type { LineToken } from './types.ts';
|
|
4
|
+
|
|
5
|
+
const TODO_COLOR = 'var(--editor-todo)';
|
|
6
|
+
const ITALIC = 1;
|
|
7
|
+
/* The marks in the scroll track read the whole document, so they wait for a pause in editing as the folds do. */
|
|
8
|
+
const TODO_DELAY_MS = 300;
|
|
9
|
+
const TODO_LIMIT = 2_000_000;
|
|
10
|
+
const CACHED_LINES = 600;
|
|
11
|
+
|
|
12
|
+
/* The text is kept with the mark, since the list is only read again after a pause and the offsets may be stale until then. */
|
|
13
|
+
export type TodoMark = TodoMatch & { readonly text: string };
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* The TODO comments of a view: drawn in their own color over the grammar's on the lines being painted,
|
|
17
|
+
* and listed for the whole document a moment after the last edit, for the ticks in the scroll track.
|
|
18
|
+
*/
|
|
19
|
+
export class TodoMarks {
|
|
20
|
+
private readonly model: DocumentModel;
|
|
21
|
+
private readonly changed: () => void;
|
|
22
|
+
private words: readonly string[] = DEFAULT_TODO_WORDS;
|
|
23
|
+
/* Empty until the first `configure`, which reads the document. */
|
|
24
|
+
private language = '';
|
|
25
|
+
private revision = -1;
|
|
26
|
+
private readonly lines = new Map<number, { base: readonly LineToken[] | null; result: readonly LineToken[] | null }>();
|
|
27
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
28
|
+
private all: readonly TodoMark[] = [];
|
|
29
|
+
/* Counts every new list, so a painter can tell its ticks are stale. */
|
|
30
|
+
version = 0;
|
|
31
|
+
|
|
32
|
+
constructor(model: DocumentModel, changed: () => void) {
|
|
33
|
+
this.model = model;
|
|
34
|
+
this.changed = changed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get marks(): readonly TodoMark[] {
|
|
38
|
+
return this.all;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
configure(language: string | undefined, words: readonly string[] | undefined): void {
|
|
42
|
+
const nextLanguage = language ?? 'plaintext';
|
|
43
|
+
const nextWords = words ?? DEFAULT_TODO_WORDS;
|
|
44
|
+
|
|
45
|
+
if (nextLanguage === this.language && nextWords.join('\n') === this.words.join('\n')) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.language = nextLanguage;
|
|
50
|
+
this.words = nextWords;
|
|
51
|
+
this.revision = -1;
|
|
52
|
+
this.refresh();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* The line's tokens with its TODOs drawn over them, the same array for as long as neither changed. */
|
|
56
|
+
styled(line: number, base: readonly LineToken[] | null): readonly LineToken[] | null {
|
|
57
|
+
if (this.words.length === 0) {
|
|
58
|
+
return base;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const revision = this.model.getRevision();
|
|
62
|
+
|
|
63
|
+
if (revision !== this.revision || this.lines.size > CACHED_LINES) {
|
|
64
|
+
this.revision = revision;
|
|
65
|
+
this.lines.clear();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const cached = this.lines.get(line);
|
|
69
|
+
|
|
70
|
+
if (cached !== undefined && cached.base === base) {
|
|
71
|
+
return cached.result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const matches = this.model.findTodos(this.language, { words: this.words }, line, line);
|
|
75
|
+
let result = base;
|
|
76
|
+
|
|
77
|
+
if (matches.length > 0) {
|
|
78
|
+
const { start, end } = this.model.getLine(line);
|
|
79
|
+
const spans = matches.map((match) => ({ from: match.from - start, to: match.to - start, color: TODO_COLOR, fontStyle: ITALIC }));
|
|
80
|
+
result = overlayTokens(base ?? [{ length: end - start, color: '', fontStyle: 0 }], spans);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.lines.set(line, { base, result });
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
schedule(): void {
|
|
89
|
+
clearTimeout(this.timer);
|
|
90
|
+
this.timer = setTimeout(() => this.refresh(), TODO_DELAY_MS);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Reads the TODOs of the whole document now, which otherwise waits for a pause in editing. */
|
|
94
|
+
refresh(): void {
|
|
95
|
+
clearTimeout(this.timer);
|
|
96
|
+
const found = this.words.length === 0 || this.model.getLength() > TODO_LIMIT ? [] : this.model.findTodos(this.language, { words: this.words });
|
|
97
|
+
const next = found.map((match) => ({ ...match, text: this.model.slice(match.from, match.to) }));
|
|
98
|
+
|
|
99
|
+
if (next.length === 0 && this.all.length === 0) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.all = next;
|
|
104
|
+
this.version++;
|
|
105
|
+
this.changed();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
dispose(): void {
|
|
109
|
+
clearTimeout(this.timer);
|
|
110
|
+
}
|
|
111
|
+
}
|