@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/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
- hints.map((hint, index) => ({
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
- this.inlayHints = hints;
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,
@@ -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
- shrinkSelection: bind('Alt+ArrowDown'),
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
- selectTextStart: bind('Meta+Shift+ArrowUp', null),
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('Meta+Shift+ArrowDown', null),
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
- : Math.max(8, metrics.measureInlay(shown) + 12) + (inlay.gapLeft ?? 0) + (inlay.gapRight ?? 0);
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 ? 'se-ghost' : box.inlay.composition ? 'se-composition' : 'se-inlay';
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 (parts === undefined) {
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
- setInlayHints(hints: readonly EditorInlayHint[]): void;
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.