@adecore/editor-react 0.22.0 → 0.23.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.
Files changed (196) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +88 -0
  3. package/dist/CompletionPopup.js +11 -3
  4. package/dist/DiffView.d.ts +22 -0
  5. package/dist/DiffView.js +203 -0
  6. package/dist/EditorContextMenu.js +1 -0
  7. package/dist/EditorView.js +18 -1
  8. package/dist/FindReplace.d.ts +1 -1
  9. package/dist/FindReplace.js +41 -11
  10. package/dist/HoverCard.js +2 -2
  11. package/dist/LanguagePopups.js +4 -1
  12. package/dist/Markdown.d.ts +2 -1
  13. package/dist/Markdown.js +8 -4
  14. package/dist/PeekPanel.d.ts +3 -0
  15. package/dist/PeekPanel.js +2 -3
  16. package/dist/PickPopup.js +15 -2
  17. package/dist/RenameCard.js +10 -5
  18. package/dist/RenderedDocs.d.ts +9 -0
  19. package/dist/RenderedDocs.js +84 -0
  20. package/dist/SplitDiffView.d.ts +9 -0
  21. package/dist/SplitDiffView.js +276 -0
  22. package/dist/SymbolPicker.d.ts +3 -0
  23. package/dist/SymbolPicker.js +4 -3
  24. package/dist/call-hierarchy-model.d.ts +52 -0
  25. package/dist/call-hierarchy-model.js +99 -0
  26. package/dist/call-hierarchy.d.ts +42 -0
  27. package/dist/call-hierarchy.js +399 -0
  28. package/dist/code-actions-model.d.ts +2 -0
  29. package/dist/code-actions-model.js +12 -10
  30. package/dist/code-actions.js +3 -1
  31. package/dist/completion-model.d.ts +14 -4
  32. package/dist/completion-model.js +40 -33
  33. package/dist/completion.d.ts +4 -0
  34. package/dist/completion.js +257 -42
  35. package/dist/diagnostics.js +8 -6
  36. package/dist/diff-expansion.d.ts +9 -0
  37. package/dist/diff-expansion.js +41 -0
  38. package/dist/diff-overlays.d.ts +15 -0
  39. package/dist/diff-overlays.js +28 -0
  40. package/dist/diff-syntax.d.ts +16 -0
  41. package/dist/diff-syntax.js +49 -0
  42. package/dist/diff-wordmarks.d.ts +5 -0
  43. package/dist/diff-wordmarks.js +26 -0
  44. package/dist/doc-comment-model.d.ts +23 -0
  45. package/dist/doc-comment-model.js +291 -0
  46. package/dist/editor-language.d.ts +13 -1
  47. package/dist/editor-language.js +14 -4
  48. package/dist/editor-react.css +79 -0
  49. package/dist/format-on-type.d.ts +14 -0
  50. package/dist/format-on-type.js +96 -0
  51. package/dist/host-types.d.ts +17 -0
  52. package/dist/hover.d.ts +4 -0
  53. package/dist/hover.js +15 -2
  54. package/dist/index.d.ts +19 -5
  55. package/dist/index.js +9 -1
  56. package/dist/inlay-hints.d.ts +47 -2
  57. package/dist/inlay-hints.js +353 -16
  58. package/dist/language-ids.js +4 -2
  59. package/dist/locales/en.json +114 -3
  60. package/dist/locales/nl.json +114 -3
  61. package/dist/models.d.ts +6 -2
  62. package/dist/models.js +5 -1
  63. package/dist/patch-decorations.d.ts +27 -0
  64. package/dist/patch-decorations.js +182 -0
  65. package/dist/patch-layout.d.ts +40 -0
  66. package/dist/patch-layout.js +304 -0
  67. package/dist/peek.js +1 -0
  68. package/dist/pick.d.ts +1 -0
  69. package/dist/pick.js +11 -1
  70. package/dist/popups.d.ts +17 -0
  71. package/dist/popups.js +1 -0
  72. package/dist/project-language.d.ts +8 -0
  73. package/dist/project-language.js +34 -2
  74. package/dist/refresher.d.ts +1 -0
  75. package/dist/refresher.js +5 -0
  76. package/dist/rename-model.d.ts +1 -1
  77. package/dist/rename-model.js +3 -7
  78. package/dist/rename.js +5 -8
  79. package/dist/rendered-docs.d.ts +62 -0
  80. package/dist/rendered-docs.js +368 -0
  81. package/dist/row-host.d.ts +2 -0
  82. package/dist/row-host.js +11 -3
  83. package/dist/semantic-delta.d.ts +2 -0
  84. package/dist/semantic-delta.js +27 -0
  85. package/dist/semantic-model.js +16 -0
  86. package/dist/semantic-tokens.d.ts +26 -1
  87. package/dist/semantic-tokens.js +174 -14
  88. package/dist/shortcut-keys.d.ts +2 -0
  89. package/dist/shortcut-keys.js +6 -2
  90. package/dist/signature.js +1 -1
  91. package/dist/snippet-field-transforms.d.ts +30 -0
  92. package/dist/snippet-field-transforms.js +116 -0
  93. package/dist/snippet-indent.d.ts +2 -0
  94. package/dist/snippet-indent.js +49 -0
  95. package/dist/snippet-range-order.d.ts +3 -0
  96. package/dist/snippet-range-order.js +19 -0
  97. package/dist/snippet-session.d.ts +42 -6
  98. package/dist/snippet-session.js +650 -53
  99. package/dist/snippet-transform-client.d.ts +19 -0
  100. package/dist/snippet-transform-client.js +66 -0
  101. package/dist/snippet-transform-format.d.ts +15 -0
  102. package/dist/snippet-transform-format.js +302 -0
  103. package/dist/snippet-transform-protocol.d.ts +45 -0
  104. package/dist/snippet-transform-protocol.js +8 -0
  105. package/dist/snippet-transform-queue.d.ts +32 -0
  106. package/dist/snippet-transform-queue.js +173 -0
  107. package/dist/snippet-transform-worker.d.ts +10 -0
  108. package/dist/snippet-transform-worker.js +41 -0
  109. package/dist/snippet-variable-names.d.ts +1 -0
  110. package/dist/snippet-variable-names.js +36 -0
  111. package/dist/snippet-variables.d.ts +7 -0
  112. package/dist/snippet-variables.js +66 -0
  113. package/dist/snippet.d.ts +30 -3
  114. package/dist/snippet.js +402 -87
  115. package/dist/split-alignment.d.ts +6 -0
  116. package/dist/split-alignment.js +38 -0
  117. package/dist/split-scroll.d.ts +14 -0
  118. package/dist/split-scroll.js +45 -0
  119. package/dist/symbols.d.ts +2 -1
  120. package/dist/symbols.js +39 -7
  121. package/dist/testing.d.ts +5 -1
  122. package/dist/testing.js +12 -0
  123. package/dist/workspace-edit.d.ts +25 -3
  124. package/dist/workspace-edit.js +282 -143
  125. package/package.json +10 -5
  126. package/src/CallHierarchyPanel.tsx +238 -0
  127. package/src/CompletionPopup.tsx +12 -1
  128. package/src/DiffView.tsx +262 -0
  129. package/src/EditorContextMenu.tsx +1 -0
  130. package/src/EditorView.tsx +21 -2
  131. package/src/FindReplace.tsx +51 -13
  132. package/src/HoverCard.tsx +23 -21
  133. package/src/LanguagePopups.tsx +5 -0
  134. package/src/Markdown.tsx +10 -3
  135. package/src/PeekPanel.tsx +2 -3
  136. package/src/PickPopup.tsx +18 -2
  137. package/src/RenameCard.tsx +18 -0
  138. package/src/RenderedDocs.tsx +155 -0
  139. package/src/SplitDiffView.tsx +337 -0
  140. package/src/SymbolPicker.tsx +4 -3
  141. package/src/call-hierarchy-model.ts +158 -0
  142. package/src/call-hierarchy.ts +441 -0
  143. package/src/code-actions-model.ts +17 -11
  144. package/src/code-actions.ts +3 -1
  145. package/src/completion-model.ts +61 -35
  146. package/src/completion.ts +302 -42
  147. package/src/diagnostics.ts +8 -6
  148. package/src/diff-expansion.ts +49 -0
  149. package/src/diff-overlays.tsx +69 -0
  150. package/src/diff-syntax.ts +67 -0
  151. package/src/diff-wordmarks.ts +31 -0
  152. package/src/doc-comment-model.ts +336 -0
  153. package/src/editor-language.ts +21 -5
  154. package/src/editor-react.css +79 -0
  155. package/src/format-on-type.ts +111 -0
  156. package/src/host-types.ts +19 -0
  157. package/src/hover.ts +18 -3
  158. package/src/index.ts +27 -4
  159. package/src/inlay-hints.ts +394 -18
  160. package/src/language-ids.ts +4 -2
  161. package/src/locales/en.json +114 -3
  162. package/src/locales/nl.json +114 -3
  163. package/src/models.ts +40 -1
  164. package/src/patch-decorations.ts +224 -0
  165. package/src/patch-layout.ts +388 -0
  166. package/src/peek.ts +1 -0
  167. package/src/pick.ts +12 -1
  168. package/src/popups.ts +24 -0
  169. package/src/project-language.ts +43 -2
  170. package/src/refresher.ts +6 -0
  171. package/src/rename-model.ts +4 -8
  172. package/src/rename.ts +5 -8
  173. package/src/rendered-docs.ts +422 -0
  174. package/src/row-host.ts +12 -3
  175. package/src/semantic-delta.ts +29 -0
  176. package/src/semantic-model.ts +17 -0
  177. package/src/semantic-tokens.ts +202 -19
  178. package/src/shortcut-keys.ts +6 -2
  179. package/src/signature.ts +1 -1
  180. package/src/snippet-field-transforms.ts +144 -0
  181. package/src/snippet-indent.ts +53 -0
  182. package/src/snippet-range-order.ts +19 -0
  183. package/src/snippet-session.ts +797 -56
  184. package/src/snippet-transform-client.ts +84 -0
  185. package/src/snippet-transform-format.ts +325 -0
  186. package/src/snippet-transform-protocol.ts +24 -0
  187. package/src/snippet-transform-queue.ts +199 -0
  188. package/src/snippet-transform-worker.ts +58 -0
  189. package/src/snippet-variable-names.ts +36 -0
  190. package/src/snippet-variables.ts +74 -0
  191. package/src/snippet.ts +452 -89
  192. package/src/split-alignment.ts +45 -0
  193. package/src/split-scroll.ts +54 -0
  194. package/src/symbols.ts +43 -7
  195. package/src/testing.ts +25 -0
  196. package/src/workspace-edit.ts +347 -169
@@ -0,0 +1,42 @@
1
+ import type { EditorPosition } from '@adecore/editor';
2
+ import type { EditorLanguage } from './editor-language.ts';
3
+ import { type CallDirection } from './call-hierarchy-model.ts';
4
+ import { type Timers } from './timers.ts';
5
+ export declare const CALL_HIERARCHY_HEIGHT: number;
6
+ export declare class CallHierarchyFeature {
7
+ private readonly language;
8
+ private readonly timers;
9
+ private nodes;
10
+ private roots;
11
+ private prepared;
12
+ private direction;
13
+ private controller;
14
+ private epoch;
15
+ private previewEpoch;
16
+ private texts;
17
+ constructor(language: EditorLanguage, timers?: Timers);
18
+ get isOpen(): boolean;
19
+ get supported(): boolean;
20
+ open(position?: EditorPosition, direction?: CallDirection): Promise<void>;
21
+ close(): void;
22
+ setDirection(direction: CallDirection): void;
23
+ toggle(id: string): void;
24
+ retry(id: string): void;
25
+ showMore(parent: string): void;
26
+ select(id: string, site?: number): void;
27
+ stepSite(direction: 1 | -1): void;
28
+ go(id: string): void;
29
+ private reset;
30
+ private seed;
31
+ private mounted;
32
+ private patch;
33
+ private load;
34
+ private preview;
35
+ private hasAncestor;
36
+ private key;
37
+ private land;
38
+ private expandOrEnter;
39
+ private collapseOrLeave;
40
+ private tell;
41
+ private say;
42
+ }
@@ -0,0 +1,399 @@
1
+ import { StaleResultError } from '@adecore/lsp';
2
+ import { CALL_LEVEL_LIMIT, childrenOf, moreIdOf, rootsOf, rowsOf, statusIdOf, stepsOf, targetOf } from './call-hierarchy-model.js';
3
+ import { PEEK_HEIGHT } from './peek.js';
4
+ import { snippetOf } from './peek-model.js';
5
+ import { isShortcut } from './shortcut-keys.js';
6
+ import { messageOf } from './error-message.js';
7
+ import { realTimers } from './timers.js';
8
+ const TOAST_ID = 'language-call-hierarchy';
9
+ const PREPARE = 'textDocument/prepareCallHierarchy';
10
+ /* The row the panel stands in is as tall as a peek's, plus the segmented control that is a pixel taller than the peek's title bar. */
11
+ export const CALL_HIERARCHY_HEIGHT = PEEK_HEIGHT + 2;
12
+ const OWNER = 'call-hierarchy';
13
+ /*
14
+ * The callers of the symbol at the caret, or the symbols it calls, as a tree between the lines of the file
15
+ * under the line the caret is on, with the code around the call that is active. A node asks the server for its
16
+ * own calls when it is opened, and a level shows its first calls with a row that brings the others.
17
+ * The editor keeps the keyboard: arrows move and open, Enter goes to the call, Escape closes, and the
18
+ * shortcut that opened the tree switches between callers and callees. Typing closes it.
19
+ */
20
+ export class CallHierarchyFeature {
21
+ language;
22
+ timers;
23
+ nodes = new Map();
24
+ roots = [];
25
+ prepared = [];
26
+ direction = 'incoming';
27
+ controller = new AbortController();
28
+ /* Raised whenever the tree starts over or closes, so an answer for the tree that was never lands in the one that is. */
29
+ epoch = 0;
30
+ previewEpoch = 0;
31
+ texts = new Map();
32
+ constructor(language, timers = realTimers) {
33
+ this.language = language;
34
+ this.timers = timers;
35
+ const { editor } = language;
36
+ const offs = [
37
+ editor.onKeyDown((event) => {
38
+ if (isShortcut(this.language.shortcuts.callHierarchy, event)) {
39
+ if (this.isOpen) {
40
+ this.setDirection(this.direction === 'incoming' ? 'outgoing' : 'incoming');
41
+ }
42
+ else {
43
+ void this.open();
44
+ }
45
+ return true;
46
+ }
47
+ return this.key(event);
48
+ }),
49
+ editor.onTextChange(() => this.close())
50
+ ];
51
+ language.onDispose(() => {
52
+ for (const off of offs) {
53
+ off();
54
+ }
55
+ this.close();
56
+ });
57
+ }
58
+ get isOpen() {
59
+ return (this.language.popups.getState().callHierarchy ?? null) !== null;
60
+ }
61
+ /* Whether a server of this document answers the three requests the tree is made of. */
62
+ get supported() {
63
+ const { project, uri } = this.language;
64
+ const { service } = project;
65
+ return (service.prepareCallHierarchy !== undefined &&
66
+ service.incomingCalls !== undefined &&
67
+ service.outgoingCalls !== undefined &&
68
+ service.supports(PREPARE, uri));
69
+ }
70
+ /* Opens the tree for the symbol at the position, on its callers unless told otherwise. */
71
+ async open(position = this.language.editor.getCaret(), direction = 'incoming') {
72
+ const { editor, project, uri } = this.language;
73
+ if (!this.supported) {
74
+ this.tell(this.say('unavailable'));
75
+ return;
76
+ }
77
+ this.close();
78
+ const epoch = this.epoch;
79
+ let items;
80
+ try {
81
+ items = await project.service.prepareCallHierarchy(uri, position, { signal: this.controller.signal });
82
+ }
83
+ catch (error) {
84
+ if (epoch === this.epoch && !(error instanceof StaleResultError)) {
85
+ this.tell(this.say('failed', { message: messageOf(error) }));
86
+ }
87
+ return;
88
+ }
89
+ if (epoch !== this.epoch) {
90
+ return;
91
+ }
92
+ if (items === null || items.length === 0) {
93
+ this.tell(this.say('none'));
94
+ return;
95
+ }
96
+ this.language.peek.close();
97
+ this.prepared = items;
98
+ this.direction = direction;
99
+ this.seed();
100
+ const active = this.roots[0];
101
+ this.language.popups.setState({
102
+ callHierarchy: { direction, container: null, name: items[0].name, rows: rowsOf(this.nodes, this.roots), active, site: 0, sites: 0, preview: null }
103
+ });
104
+ this.select(active);
105
+ // The editor draws a row once per id, so a tree opened over another needs an id of its own to get a container.
106
+ editor.setWidgets([{ id: `call-hierarchy-${epoch}`, line: position.line, height: CALL_HIERARCHY_HEIGHT, render: (container) => this.mounted(container) }], OWNER);
107
+ editor.focus();
108
+ for (const root of this.roots) {
109
+ void this.load(root);
110
+ }
111
+ }
112
+ close() {
113
+ this.reset();
114
+ if (this.language.popups.getState().callHierarchy == null) {
115
+ return;
116
+ }
117
+ this.language.popups.setState({ callHierarchy: null });
118
+ this.language.editor.setWidgets([], OWNER);
119
+ this.language.editor.setInputPopup(null);
120
+ this.language.editor.focus();
121
+ }
122
+ /* Starts the tree over from the same symbols for the other direction. */
123
+ setDirection(direction) {
124
+ const view = this.language.popups.getState().callHierarchy ?? null;
125
+ if (view === null || view.direction === direction) {
126
+ return;
127
+ }
128
+ this.reset();
129
+ this.direction = direction;
130
+ this.seed();
131
+ const active = this.roots[0];
132
+ this.language.popups.setState({
133
+ callHierarchy: { ...view, direction, rows: rowsOf(this.nodes, this.roots), active, site: 0, sites: 0, preview: null }
134
+ });
135
+ this.select(active);
136
+ for (const root of this.roots) {
137
+ void this.load(root);
138
+ }
139
+ }
140
+ /* Opens a node, asking the server for its calls the first time, or folds it away. */
141
+ toggle(id) {
142
+ const node = this.nodes.get(id);
143
+ if (node === undefined) {
144
+ return;
145
+ }
146
+ if (node.expanded) {
147
+ this.patch(id, { expanded: false });
148
+ const view = this.language.popups.getState().callHierarchy ?? null;
149
+ if (view !== null && this.hasAncestor(view.active, id)) {
150
+ this.select(id);
151
+ }
152
+ return;
153
+ }
154
+ this.patch(id, { expanded: true });
155
+ if (node.load === 'idle' || node.load === 'failed') {
156
+ void this.load(id);
157
+ }
158
+ }
159
+ /* Asks again for the calls of a node whose last try failed. */
160
+ retry(id) {
161
+ if (this.nodes.get(id)?.load === 'failed') {
162
+ void this.load(id);
163
+ }
164
+ }
165
+ /* Brings the next stretch of calls of a level. */
166
+ showMore(parent) {
167
+ const node = this.nodes.get(parent);
168
+ if (node === undefined) {
169
+ return;
170
+ }
171
+ this.patch(parent, { shown: Math.min(node.children.length, node.shown + CALL_LEVEL_LIMIT) });
172
+ const first = node.children[node.shown];
173
+ const view = this.language.popups.getState().callHierarchy ?? null;
174
+ // The row that offered more is gone once everything shows, so the selection moves on to what it brought.
175
+ if (first !== undefined && view?.active === moreIdOf(parent)) {
176
+ this.select(first);
177
+ }
178
+ }
179
+ /* Makes a row the active one and shows the code of its call. */
180
+ select(id, site = 0) {
181
+ const view = this.language.popups.getState().callHierarchy ?? null;
182
+ const node = this.nodes.get(id);
183
+ if (view === null || !view.rows.some((row) => row.id === id)) {
184
+ return;
185
+ }
186
+ const sites = node?.sites.length ?? view.sites;
187
+ const next = node === undefined ? view.site : Math.max(0, Math.min(Math.max(0, sites - 1), site));
188
+ this.language.popups.setState({ callHierarchy: { ...view, active: id, site: next, sites, preview: node === undefined ? view.preview : null } });
189
+ if (node !== undefined) {
190
+ void this.preview(node, next);
191
+ }
192
+ }
193
+ /* Steps through the places a call is made from when the active one has more than one. */
194
+ stepSite(direction) {
195
+ const view = this.language.popups.getState().callHierarchy ?? null;
196
+ if (view !== null && view.sites > 1) {
197
+ this.select(view.active, (view.site + direction + view.sites) % view.sites);
198
+ }
199
+ }
200
+ /* Goes to the call of a row, or to the symbol of a root, and closes the tree. */
201
+ go(id) {
202
+ const view = this.language.popups.getState().callHierarchy ?? null;
203
+ const node = this.nodes.get(id);
204
+ if (view === null || node === undefined) {
205
+ return;
206
+ }
207
+ const { uri, range } = targetOf(node, id === view.active ? view.site : 0);
208
+ this.close();
209
+ this.language.goTo({ uri, range });
210
+ }
211
+ reset() {
212
+ this.epoch++;
213
+ this.previewEpoch++;
214
+ this.controller.abort();
215
+ this.controller = new AbortController();
216
+ this.nodes = new Map();
217
+ this.roots = [];
218
+ this.texts = new Map();
219
+ }
220
+ seed() {
221
+ const roots = rootsOf(this.prepared);
222
+ this.roots = roots.map((root) => root.id);
223
+ this.nodes = new Map(roots.map((root) => [root.id, root]));
224
+ }
225
+ mounted(container) {
226
+ const view = this.language.popups.getState().callHierarchy ?? null;
227
+ if (view !== null && view.container !== container) {
228
+ this.language.popups.setState({ callHierarchy: { ...view, container } });
229
+ // The row opens under the caret line, which may sit at the bottom of the view with the tree below the fold. The element is in the page a turn later.
230
+ this.timers.set(() => container.scrollIntoView?.({ block: 'nearest' }), 0);
231
+ }
232
+ }
233
+ patch(id, change) {
234
+ const node = this.nodes.get(id);
235
+ const view = this.language.popups.getState().callHierarchy ?? null;
236
+ if (node === undefined || view === null) {
237
+ return;
238
+ }
239
+ this.nodes.set(id, { ...node, ...change });
240
+ this.language.popups.setState({ callHierarchy: { ...view, rows: rowsOf(this.nodes, this.roots) } });
241
+ }
242
+ async load(id) {
243
+ const node = this.nodes.get(id);
244
+ if (node === undefined || node.load === 'loading') {
245
+ return;
246
+ }
247
+ const { service, i18n } = this.language.project;
248
+ const { uri, editor } = this.language;
249
+ const { direction, epoch } = this;
250
+ const options = { signal: this.controller.signal, parallel: true };
251
+ this.patch(id, { load: 'loading', error: null });
252
+ try {
253
+ const calls = direction === 'incoming' ? await service.incomingCalls(uri, node.item, options) : await service.outgoingCalls(uri, node.item, options);
254
+ if (epoch !== this.epoch) {
255
+ return;
256
+ }
257
+ const children = childrenOf(direction, node, calls ?? []);
258
+ for (const child of children) {
259
+ this.nodes.set(child.id, child);
260
+ }
261
+ this.patch(id, { load: 'loaded', children: children.map((child) => child.id), shown: Math.min(children.length, CALL_LEVEL_LIMIT) });
262
+ editor.announce(i18n.t(`editor:language.callHierarchy.announce.${direction}`, { count: children.length, name: node.item.name }));
263
+ }
264
+ catch (error) {
265
+ if (epoch !== this.epoch) {
266
+ return;
267
+ }
268
+ if (error instanceof StaleResultError) {
269
+ this.close();
270
+ return;
271
+ }
272
+ this.patch(id, { load: 'failed', error: messageOf(error) });
273
+ }
274
+ }
275
+ async preview(node, site) {
276
+ const { project } = this.language;
277
+ const epoch = ++this.previewEpoch;
278
+ const { uri, range } = targetOf(node, site);
279
+ let text = this.texts.get(uri);
280
+ if (text === undefined) {
281
+ try {
282
+ text = (await project.readText(uri)) ?? undefined;
283
+ }
284
+ catch {
285
+ // A file that cannot be read shows no code, as one that is no text file does.
286
+ text = undefined;
287
+ }
288
+ if (epoch !== this.previewEpoch) {
289
+ return;
290
+ }
291
+ if (text !== undefined) {
292
+ this.texts.set(uri, text);
293
+ }
294
+ }
295
+ const view = this.language.popups.getState().callHierarchy ?? null;
296
+ if (view === null || view.active !== node.id) {
297
+ return;
298
+ }
299
+ const snippet = text === undefined ? null : snippetOf(text, range.start.line, range);
300
+ this.language.popups.setState({ callHierarchy: { ...view, preview: snippet === null ? null : { ...snippet, uri } } });
301
+ }
302
+ /* Whether a row stands under a node; the `more` and `status` rows belong to the node they are about. */
303
+ hasAncestor(id, ancestor) {
304
+ const owner = [moreIdOf(''), statusIdOf('')]
305
+ .map((suffix) => (id.endsWith(suffix) ? id.slice(0, -suffix.length) : null))
306
+ .find((found) => found !== null);
307
+ if (owner !== undefined && owner !== null) {
308
+ return owner === ancestor || this.hasAncestor(owner, ancestor);
309
+ }
310
+ for (let node = this.nodes.get(id); node !== undefined && node.parent !== null; node = this.nodes.get(node.parent)) {
311
+ if (node.parent === ancestor) {
312
+ return true;
313
+ }
314
+ }
315
+ return false;
316
+ }
317
+ key(event) {
318
+ const view = this.language.popups.getState().callHierarchy ?? null;
319
+ if (view === null || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.isComposing) {
320
+ return false;
321
+ }
322
+ const steps = stepsOf(view.rows);
323
+ const index = steps.findIndex((row) => row.id === view.active);
324
+ const row = steps[index];
325
+ switch (event.key) {
326
+ case 'ArrowDown':
327
+ this.land(steps[Math.min(steps.length - 1, index + 1)]);
328
+ return true;
329
+ case 'ArrowUp':
330
+ this.land(steps[Math.max(0, index - 1)]);
331
+ return true;
332
+ case 'Home':
333
+ this.land(steps[0]);
334
+ return true;
335
+ case 'End':
336
+ this.land(steps.at(-1));
337
+ return true;
338
+ case 'ArrowRight':
339
+ this.expandOrEnter(row, steps[index + 1]);
340
+ return true;
341
+ case 'ArrowLeft':
342
+ this.collapseOrLeave(row);
343
+ return true;
344
+ case 'Enter':
345
+ if (row?.kind === 'more') {
346
+ this.showMore(row.parent);
347
+ }
348
+ else if (row !== undefined) {
349
+ this.go(row.id);
350
+ }
351
+ return true;
352
+ case 'Escape':
353
+ this.close();
354
+ return true;
355
+ default:
356
+ return false;
357
+ }
358
+ }
359
+ land(row) {
360
+ if (row !== undefined) {
361
+ this.select(row.id);
362
+ }
363
+ }
364
+ /* Right opens a closed node, tries a failed one again, and steps into an open one. */
365
+ expandOrEnter(row, next) {
366
+ if (row?.kind !== 'node') {
367
+ return;
368
+ }
369
+ if (!row.expanded) {
370
+ this.toggle(row.id);
371
+ }
372
+ else if (row.load === 'failed') {
373
+ this.retry(row.id);
374
+ }
375
+ else if (next !== undefined && next.level > row.level) {
376
+ this.land(next);
377
+ }
378
+ }
379
+ /* Left closes an open node and steps out of a closed one. */
380
+ collapseOrLeave(row) {
381
+ if (row === undefined) {
382
+ return;
383
+ }
384
+ if (row.kind === 'node' && row.expanded && !row.leaf) {
385
+ this.toggle(row.id);
386
+ return;
387
+ }
388
+ const parent = row.kind === 'node' ? (this.nodes.get(row.id)?.parent ?? null) : row.parent;
389
+ if (parent !== null) {
390
+ this.select(parent);
391
+ }
392
+ }
393
+ tell(title) {
394
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title });
395
+ }
396
+ say(key, options) {
397
+ return this.language.project.i18n.t(`editor:language.callHierarchy.${key}`, options);
398
+ }
399
+ }
@@ -19,5 +19,7 @@ export interface EditPreview {
19
19
  readonly hiddenLines: number;
20
20
  readonly otherFiles: number;
21
21
  readonly moves: number;
22
+ readonly creates?: number;
23
+ readonly deletes?: number;
22
24
  }
23
25
  export declare function previewOf(text: string, edit: WorkspaceEdit, uri: string): EditPreview | null;
@@ -1,6 +1,6 @@
1
1
  import { applyTextEdits } from '@adecore/lsp';
2
2
  import { comparePositions } from './diagnostics-model.js';
3
- import { entriesOf, renamesOf } from './workspace-edit.js';
3
+ import { resourceOperationsOf, textEntriesOf } from './workspace-edit.js';
4
4
  /* The order of the list under the caret: the fixes and the rewrites that are one keystroke away first, the refactorings after them. */
5
5
  export const ACTION_GROUPS = ['quickfix', 'rewrite', 'extract', 'inline', 'move', 'refactor', 'source', 'other'];
6
6
  /* The order of Refactor This: what changes the structure of the code first, the rewrites after it. */
@@ -75,15 +75,17 @@ const PREVIEW_LINES = 3;
75
75
  function linesOf(text) {
76
76
  return text.split(/\r\n|\r|\n/);
77
77
  }
78
- /* Null when the edit changes nothing, or deletes a file, which a preview of lines cannot show. A file it creates counts as another file. */
78
+ /* Null when the edit changes nothing. A file it creates is counted under `creates` and not as another file; moves and deletes are counted apart. */
79
79
  export function previewOf(text, edit, uri) {
80
- const entries = entriesOf(edit);
81
- if (entries === null) {
82
- return null;
83
- }
80
+ const entries = textEntriesOf(edit);
81
+ const operations = resourceOperationsOf(edit);
82
+ const count = (kind) => operations.filter((operation) => operation.kind === kind).length;
83
+ const moves = count('rename');
84
+ const creates = count('create');
85
+ const deletes = count('delete');
84
86
  const own = entries.get(uri);
85
- const moves = renamesOf(edit).length;
86
- const otherFiles = [...entries.keys()].filter((key) => key !== uri).length;
87
+ const made = new Set(operations.filter((operation) => operation.kind === 'create').map((operation) => operation.uri));
88
+ const otherFiles = [...entries.keys()].filter((key) => key !== uri && !made.has(key)).length;
87
89
  let removed = [];
88
90
  let added = [];
89
91
  if (own !== undefined) {
@@ -107,9 +109,9 @@ export function previewOf(text, edit, uri) {
107
109
  removed = before.slice(head, before.length - tail);
108
110
  added = next.slice(head, next.length - tail);
109
111
  }
110
- if (removed.length === 0 && added.length === 0 && otherFiles === 0 && moves === 0) {
112
+ if (removed.length === 0 && added.length === 0 && otherFiles === 0 && moves === 0 && creates === 0 && deletes === 0) {
111
113
  return null;
112
114
  }
113
115
  const hiddenLines = Math.max(0, removed.length - PREVIEW_LINES) + Math.max(0, added.length - PREVIEW_LINES);
114
- return { removed: removed.slice(0, PREVIEW_LINES), added: added.slice(0, PREVIEW_LINES), hiddenLines, otherFiles, moves };
116
+ return { removed: removed.slice(0, PREVIEW_LINES), added: added.slice(0, PREVIEW_LINES), hiddenLines, otherFiles, moves, creates, deletes };
115
117
  }
@@ -261,7 +261,9 @@ export class CodeActionsFeature {
261
261
  const notes = [
262
262
  ...(found.hiddenLines > 0 ? [this.say('moreLines', { count: found.hiddenLines })] : []),
263
263
  ...(found.otherFiles > 0 ? [this.say('otherFiles', { count: found.otherFiles })] : []),
264
- ...(found.moves > 0 ? [this.say('moves', { count: found.moves })] : [])
264
+ ...(found.moves > 0 ? [this.say('moves', { count: found.moves })] : []),
265
+ ...((found.creates ?? 0) > 0 ? [this.say('creates', { count: found.creates })] : []),
266
+ ...((found.deletes ?? 0) > 0 ? [this.say('deletes', { count: found.deletes })] : [])
265
267
  ];
266
268
  return { removed: found.removed, added: found.added, note: notes.length === 0 ? null : notes.join(' · ') };
267
269
  }
@@ -1,7 +1,8 @@
1
+ import type { SnippetTransformJob } from './snippet-transform-protocol.ts';
1
2
  import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
2
- import type { CompletionItem, CompletionResult, MarkupContent } from '@adecore/lsp';
3
+ import { type CompletionItem, type CompletionResult, type MarkupContent } from '@adecore/lsp';
3
4
  import type { CompletionDocs } from './popups.ts';
4
- import { type SnippetStop } from './snippet.ts';
5
+ import { type SnippetField, type SnippetStop, type SnippetTemplate, type SnippetVariableResolver } from './snippet.ts';
5
6
  export declare function isIdentifierCharacter(character: string): boolean;
6
7
  export declare function identifierPrefix(before: string): string;
7
8
  export declare function opensCompletion(before: string, text: string): boolean;
@@ -34,12 +35,21 @@ export declare const CLASS_KIND = 7;
34
35
  export declare const SNIPPET_FORMAT = 2;
35
36
  export declare function isCallableKind(kind: number | undefined): boolean;
36
37
  export interface Insertion {
38
+ readonly limited?: true;
37
39
  readonly range: EditorRange;
38
40
  readonly text: string;
39
41
  readonly stops: readonly SnippetStop[];
42
+ readonly fields?: readonly SnippetField[];
43
+ readonly variableTransforms?: readonly SnippetTransformJob[];
40
44
  }
41
- export declare function insertionOf(item: CompletionItem, caret: EditorPosition, lineBefore: string, replace: boolean, lineAfter?: string): Insertion;
42
- export declare function mirroredInsertions(carets: readonly EditorRange[], primary: EditorRange, caret: EditorPosition, text: (range: EditorRange) => string, insertion: Insertion): EditorContentChange[];
45
+ export interface SnippetInsertionOptions {
46
+ readonly resolve?: SnippetVariableResolver;
47
+ readonly variableTransforms?: readonly string[];
48
+ readonly template?: SnippetTemplate;
49
+ readonly indentationBefore?: string;
50
+ }
51
+ export declare function insertionOf(item: CompletionItem, caret: EditorPosition, lineBefore: string, replace: boolean, lineAfter?: string, options?: SnippetInsertionOptions): Insertion;
52
+ export declare function mirroredInsertions(carets: readonly EditorRange[], primary: EditorRange, caret: EditorPosition, text: (range: EditorRange) => string, insertion: Insertion, includeSelections?: boolean): EditorContentChange[];
43
53
  export declare function documentationText(documentation: string | MarkupContent | undefined): string;
44
54
  export declare const SOURCE_LINE: RegExp;
45
55
  export declare function completionDocsOf(item: CompletionItem, highlightLanguage: string): CompletionDocs | null;
@@ -1,6 +1,8 @@
1
+ import { applyCompletionDefaults } from '@adecore/lsp';
1
2
  import { comparePositions } from './diagnostics-model.js';
2
3
  import { escapeMarkdown, isEmptyHover, splitSignatures } from './hover-content.js';
3
- import { parseSnippet, tabOrder } from './snippet.js';
4
+ import { compileSnippet, parseSnippet, tabOrder } from './snippet.js';
5
+ import { indentSnippet } from './snippet-indent.js';
4
6
  const IDENTIFIER_CHARACTER = /[\p{L}\p{N}\p{M}_$]/u;
5
7
  export function isIdentifierCharacter(character) {
6
8
  return IDENTIFIER_CHARACTER.test(character);
@@ -117,35 +119,23 @@ export function editRangeOf(item, replace) {
117
119
  }
118
120
  /* Items from a result, with the list's defaults filled in so each carries its own edit range and format. */
119
121
  export function itemsOf(result) {
120
- if (result === null) {
121
- return { items: [], incomplete: false };
122
- }
123
- if (Array.isArray(result)) {
124
- return { items: result, incomplete: false };
125
- }
126
- const list = result;
127
- const defaults = list.itemDefaults;
128
- if (defaults === undefined) {
129
- return { items: list.items, incomplete: list.isIncomplete };
130
- }
122
+ const list = result === null || Array.isArray(result) ? undefined : result;
123
+ const items = result === null ? [] : Array.isArray(result) ? result : result.items;
131
124
  return {
132
- incomplete: list.isIncomplete,
133
- items: list.items.map((item) => {
134
- const next = { ...item };
135
- if (defaults.insertTextFormat !== undefined && next.insertTextFormat === undefined) {
136
- next.insertTextFormat = defaults.insertTextFormat;
125
+ incomplete: list?.isIncomplete ?? false,
126
+ items: items.flatMap((item) => {
127
+ if (typeof item?.label !== 'string') {
128
+ return [];
137
129
  }
138
- if (defaults.commitCharacters !== undefined && next.commitCharacters === undefined) {
139
- next.commitCharacters = defaults.commitCharacters;
130
+ try {
131
+ return [applyCompletionDefaults(item, list?.itemDefaults, { onInvalidRange: () => undefined })];
140
132
  }
141
- if (next.textEdit === undefined && defaults.editRange !== undefined) {
142
- const text = item.textEditText ?? item.insertText ?? item.label;
143
- next.textEdit =
144
- 'start' in defaults.editRange
145
- ? { range: defaults.editRange, newText: text }
146
- : { insert: defaults.editRange.insert, replace: defaults.editRange.replace, newText: text };
133
+ catch (error) {
134
+ if (error instanceof TypeError || error instanceof RangeError) {
135
+ return [];
136
+ }
137
+ throw error;
147
138
  }
148
- return next;
149
139
  })
150
140
  };
151
141
  }
@@ -236,7 +226,7 @@ function withoutCall(snippet) {
236
226
  * What goes into the text for an item and the range it takes the place of, given the caret. `lineAfter` is
237
227
  * the rest of the line after the caret: an item that would add a call does not when a parenthesis follows.
238
228
  */
239
- export function insertionOf(item, caret, lineBefore, replace, lineAfter = '') {
229
+ export function insertionOf(item, caret, lineBefore, replace, lineAfter = '', options = {}) {
240
230
  const raw = item.textEdit?.newText ?? item.insertText ?? item.label;
241
231
  const edit = editRangeOf(item, replace);
242
232
  const range = edit === null
@@ -244,17 +234,34 @@ export function insertionOf(item, caret, lineBefore, replace, lineAfter = '') {
244
234
  : // The word grew since the answer came, so what was typed since belongs to what is replaced.
245
235
  { start: edit.start, end: comparePositions(edit.end, caret) < 0 && edit.end.line === caret.line ? caret : edit.end };
246
236
  if (item.insertTextFormat !== SNIPPET_FORMAT) {
247
- return { range, text: raw, stops: [] };
237
+ const adjusted = item.insertTextMode === 2
238
+ ? indentSnippet({ text: raw, stops: [] }, options.indentationBefore ?? lineBefore.slice(0, range.start.character))
239
+ : { text: raw, stops: [] };
240
+ return { range, text: adjusted.text, stops: [], ...(adjusted.limited ? { limited: true } : {}) };
248
241
  }
249
242
  const followed = range.end.line === caret.line && lineAfter.slice(Math.max(0, range.end.character - caret.character)).startsWith('(');
250
- const parsed = parseSnippet(followed && isCallableKind(item.kind) ? withoutCall(raw) : raw);
251
- return { range, text: parsed.text, stops: tabOrder(parsed) };
243
+ const source = followed && isCallableKind(item.kind) ? withoutCall(raw) : raw;
244
+ const template = options.template?.source === source ? options.template : compileSnippet(source);
245
+ const rendered = template.render(options.resolve, options.variableTransforms);
246
+ const parsed = item.insertTextMode === 2 ? indentSnippet(rendered, options.indentationBefore ?? lineBefore.slice(0, range.start.character)) : rendered;
247
+ return {
248
+ range,
249
+ text: parsed.text,
250
+ stops: tabOrder(parsed),
251
+ fields: parsed.fields,
252
+ ...(options.variableTransforms !== undefined || rendered.transforms === undefined
253
+ ? {}
254
+ : {
255
+ variableTransforms: rendered.transforms.map((range) => ({ ...range.transform, value: rendered.text.slice(range.start, range.end) }))
256
+ }),
257
+ ...(parsed.limited ? { limited: true } : {})
258
+ };
252
259
  }
253
260
  /*
254
261
  * The same insertion at every other caret that has the word the primary caret has before it, so a name typed at
255
262
  * several carets is completed at all of them. A caret that is inside a selection, or after other text, is left alone.
256
263
  */
257
- export function mirroredInsertions(carets, primary, caret, text, insertion) {
264
+ export function mirroredInsertions(carets, primary, caret, text, insertion, includeSelections = false) {
258
265
  const before = insertion.range.start.line === caret.line ? caret.character - insertion.range.start.character : -1;
259
266
  const after = insertion.range.end.line === caret.line ? insertion.range.end.character - caret.character : -1;
260
267
  if (before < 0 || after < 0) {
@@ -263,11 +270,11 @@ export function mirroredInsertions(carets, primary, caret, text, insertion) {
263
270
  const typed = text({ start: insertion.range.start, end: caret });
264
271
  const replaced = text({ start: caret, end: insertion.range.end });
265
272
  return carets.flatMap((other) => {
266
- const at = other.start;
273
+ const at = includeSelections ? other.end : other.start;
267
274
  if (comparePositions(other.start, primary.start) === 0 && comparePositions(other.end, primary.end) === 0) {
268
275
  return [];
269
276
  }
270
- if (comparePositions(other.start, other.end) !== 0 || at.character < before) {
277
+ if ((!includeSelections && comparePositions(other.start, other.end) !== 0) || at.character < before) {
271
278
  return [];
272
279
  }
273
280
  const range = { start: { line: at.line, character: at.character - before }, end: { line: at.line, character: at.character + after } };