@adecore/editor-react 0.22.1 → 0.23.1

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 (209) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +92 -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/PanelResizeHandle.d.ts +8 -0
  15. package/dist/PanelResizeHandle.js +71 -0
  16. package/dist/PeekPanel.d.ts +3 -0
  17. package/dist/PeekPanel.js +7 -4
  18. package/dist/PickPopup.js +15 -2
  19. package/dist/RenameCard.js +10 -5
  20. package/dist/RenderedDocs.d.ts +9 -0
  21. package/dist/RenderedDocs.js +84 -0
  22. package/dist/SplitDiffView.d.ts +9 -0
  23. package/dist/SplitDiffView.js +276 -0
  24. package/dist/SymbolPicker.d.ts +3 -0
  25. package/dist/SymbolPicker.js +4 -3
  26. package/dist/call-hierarchy-model.d.ts +52 -0
  27. package/dist/call-hierarchy-model.js +93 -0
  28. package/dist/call-hierarchy.d.ts +42 -0
  29. package/dist/call-hierarchy.js +398 -0
  30. package/dist/code-actions-model.d.ts +2 -0
  31. package/dist/code-actions-model.js +12 -10
  32. package/dist/code-actions.js +3 -1
  33. package/dist/completion-model.d.ts +14 -4
  34. package/dist/completion-model.js +40 -33
  35. package/dist/completion.d.ts +4 -0
  36. package/dist/completion.js +257 -42
  37. package/dist/diagnostics.js +8 -6
  38. package/dist/diff-expansion.d.ts +9 -0
  39. package/dist/diff-expansion.js +41 -0
  40. package/dist/diff-overlays.d.ts +15 -0
  41. package/dist/diff-overlays.js +28 -0
  42. package/dist/diff-syntax.d.ts +16 -0
  43. package/dist/diff-syntax.js +49 -0
  44. package/dist/diff-wordmarks.d.ts +5 -0
  45. package/dist/diff-wordmarks.js +26 -0
  46. package/dist/doc-comment-model.d.ts +23 -0
  47. package/dist/doc-comment-model.js +291 -0
  48. package/dist/editor-language.d.ts +13 -1
  49. package/dist/editor-language.js +15 -5
  50. package/dist/editor-react.css +79 -0
  51. package/dist/format-on-type.d.ts +14 -0
  52. package/dist/format-on-type.js +96 -0
  53. package/dist/host-types.d.ts +19 -0
  54. package/dist/hover.d.ts +4 -0
  55. package/dist/hover.js +15 -2
  56. package/dist/index.d.ts +19 -5
  57. package/dist/index.js +9 -1
  58. package/dist/inlay-hints.d.ts +47 -2
  59. package/dist/inlay-hints.js +353 -16
  60. package/dist/language-ids.js +4 -2
  61. package/dist/locales/en.json +117 -3
  62. package/dist/locales/nl.json +117 -3
  63. package/dist/models.d.ts +6 -2
  64. package/dist/models.js +5 -1
  65. package/dist/panel-size.d.ts +6 -0
  66. package/dist/panel-size.js +25 -0
  67. package/dist/patch-decorations.d.ts +27 -0
  68. package/dist/patch-decorations.js +182 -0
  69. package/dist/patch-layout.d.ts +40 -0
  70. package/dist/patch-layout.js +304 -0
  71. package/dist/peek.d.ts +3 -1
  72. package/dist/peek.js +6 -1
  73. package/dist/pick.d.ts +1 -0
  74. package/dist/pick.js +11 -1
  75. package/dist/popups.d.ts +17 -0
  76. package/dist/popups.js +1 -0
  77. package/dist/project-language.d.ts +12 -0
  78. package/dist/project-language.js +52 -2
  79. package/dist/refresher.d.ts +1 -0
  80. package/dist/refresher.js +5 -0
  81. package/dist/rename-model.d.ts +1 -1
  82. package/dist/rename-model.js +3 -7
  83. package/dist/rename.js +5 -8
  84. package/dist/rendered-docs.d.ts +64 -0
  85. package/dist/rendered-docs.js +374 -0
  86. package/dist/row-host.d.ts +2 -0
  87. package/dist/row-host.js +11 -3
  88. package/dist/semantic-delta.d.ts +2 -0
  89. package/dist/semantic-delta.js +27 -0
  90. package/dist/semantic-model.js +16 -0
  91. package/dist/semantic-tokens.d.ts +26 -1
  92. package/dist/semantic-tokens.js +174 -14
  93. package/dist/shortcut-keys.d.ts +2 -0
  94. package/dist/shortcut-keys.js +6 -2
  95. package/dist/signature.js +1 -1
  96. package/dist/snippet-field-transforms.d.ts +30 -0
  97. package/dist/snippet-field-transforms.js +116 -0
  98. package/dist/snippet-indent.d.ts +2 -0
  99. package/dist/snippet-indent.js +49 -0
  100. package/dist/snippet-range-order.d.ts +3 -0
  101. package/dist/snippet-range-order.js +19 -0
  102. package/dist/snippet-session.d.ts +42 -6
  103. package/dist/snippet-session.js +650 -53
  104. package/dist/snippet-transform-client.d.ts +19 -0
  105. package/dist/snippet-transform-client.js +66 -0
  106. package/dist/snippet-transform-format.d.ts +15 -0
  107. package/dist/snippet-transform-format.js +302 -0
  108. package/dist/snippet-transform-protocol.d.ts +45 -0
  109. package/dist/snippet-transform-protocol.js +8 -0
  110. package/dist/snippet-transform-queue.d.ts +32 -0
  111. package/dist/snippet-transform-queue.js +173 -0
  112. package/dist/snippet-transform-worker.d.ts +10 -0
  113. package/dist/snippet-transform-worker.js +41 -0
  114. package/dist/snippet-variable-names.d.ts +1 -0
  115. package/dist/snippet-variable-names.js +36 -0
  116. package/dist/snippet-variables.d.ts +7 -0
  117. package/dist/snippet-variables.js +66 -0
  118. package/dist/snippet.d.ts +30 -3
  119. package/dist/snippet.js +402 -87
  120. package/dist/split-alignment.d.ts +6 -0
  121. package/dist/split-alignment.js +38 -0
  122. package/dist/split-scroll.d.ts +14 -0
  123. package/dist/split-scroll.js +45 -0
  124. package/dist/symbols.d.ts +2 -1
  125. package/dist/symbols.js +39 -7
  126. package/dist/testing.d.ts +5 -1
  127. package/dist/testing.js +12 -0
  128. package/dist/timers.d.ts +1 -0
  129. package/dist/timers.js +4 -0
  130. package/dist/use-panel-body.d.ts +6 -0
  131. package/dist/use-panel-body.js +13 -0
  132. package/dist/workspace-edit.d.ts +25 -3
  133. package/dist/workspace-edit.js +282 -143
  134. package/package.json +10 -5
  135. package/src/CallHierarchyPanel.tsx +244 -0
  136. package/src/CompletionPopup.tsx +12 -1
  137. package/src/DiffView.tsx +262 -0
  138. package/src/EditorContextMenu.tsx +1 -0
  139. package/src/EditorView.tsx +21 -2
  140. package/src/FindReplace.tsx +51 -13
  141. package/src/HoverCard.tsx +23 -21
  142. package/src/LanguagePopups.tsx +5 -0
  143. package/src/Markdown.tsx +10 -3
  144. package/src/PanelResizeHandle.tsx +104 -0
  145. package/src/PeekPanel.tsx +11 -6
  146. package/src/PickPopup.tsx +18 -2
  147. package/src/RenameCard.tsx +18 -0
  148. package/src/RenderedDocs.tsx +155 -0
  149. package/src/SplitDiffView.tsx +337 -0
  150. package/src/SymbolPicker.tsx +4 -3
  151. package/src/call-hierarchy-model.ts +151 -0
  152. package/src/call-hierarchy.ts +440 -0
  153. package/src/code-actions-model.ts +17 -11
  154. package/src/code-actions.ts +3 -1
  155. package/src/completion-model.ts +61 -35
  156. package/src/completion.ts +302 -42
  157. package/src/diagnostics.ts +8 -6
  158. package/src/diff-expansion.ts +49 -0
  159. package/src/diff-overlays.tsx +69 -0
  160. package/src/diff-syntax.ts +67 -0
  161. package/src/diff-wordmarks.ts +31 -0
  162. package/src/doc-comment-model.ts +336 -0
  163. package/src/editor-language.ts +22 -6
  164. package/src/editor-react.css +79 -0
  165. package/src/format-on-type.ts +111 -0
  166. package/src/host-types.ts +23 -0
  167. package/src/hover.ts +18 -3
  168. package/src/index.ts +27 -4
  169. package/src/inlay-hints.ts +394 -18
  170. package/src/language-ids.ts +4 -2
  171. package/src/locales/en.json +117 -3
  172. package/src/locales/nl.json +117 -3
  173. package/src/models.ts +40 -1
  174. package/src/panel-size.ts +28 -0
  175. package/src/patch-decorations.ts +224 -0
  176. package/src/patch-layout.ts +388 -0
  177. package/src/peek.ts +6 -1
  178. package/src/pick.ts +12 -1
  179. package/src/popups.ts +24 -0
  180. package/src/project-language.ts +63 -2
  181. package/src/refresher.ts +6 -0
  182. package/src/rename-model.ts +4 -8
  183. package/src/rename.ts +5 -8
  184. package/src/rendered-docs.ts +430 -0
  185. package/src/row-host.ts +12 -3
  186. package/src/semantic-delta.ts +29 -0
  187. package/src/semantic-model.ts +17 -0
  188. package/src/semantic-tokens.ts +202 -19
  189. package/src/shortcut-keys.ts +6 -2
  190. package/src/signature.ts +1 -1
  191. package/src/snippet-field-transforms.ts +144 -0
  192. package/src/snippet-indent.ts +53 -0
  193. package/src/snippet-range-order.ts +19 -0
  194. package/src/snippet-session.ts +797 -56
  195. package/src/snippet-transform-client.ts +84 -0
  196. package/src/snippet-transform-format.ts +325 -0
  197. package/src/snippet-transform-protocol.ts +24 -0
  198. package/src/snippet-transform-queue.ts +199 -0
  199. package/src/snippet-transform-worker.ts +58 -0
  200. package/src/snippet-variable-names.ts +36 -0
  201. package/src/snippet-variables.ts +74 -0
  202. package/src/snippet.ts +452 -89
  203. package/src/split-alignment.ts +45 -0
  204. package/src/split-scroll.ts +54 -0
  205. package/src/symbols.ts +43 -7
  206. package/src/testing.ts +25 -0
  207. package/src/timers.ts +5 -0
  208. package/src/use-panel-body.ts +17 -0
  209. package/src/workspace-edit.ts +347 -169
@@ -0,0 +1,337 @@
1
+ import { useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { parseChord } from '@adecore/editor/keymap';
4
+ import type { EditorLineHighlight } from '@adecore/editor';
5
+ import type { DiffSplit } from '@adecore/merge';
6
+ import type { DiffViewProps } from './DiffView.tsx';
7
+ import type { DiffSourceSide } from './diff-syntax.ts';
8
+ import { DiffOverlays, type MountedDiff } from './diff-overlays.tsx';
9
+ import { wordMarks } from './diff-wordmarks.ts';
10
+ import { RowHost, LineActionHost } from './row-host.ts';
11
+ import { splitAlignment } from './split-alignment.ts';
12
+ import { SplitScroll } from './split-scroll.ts';
13
+
14
+ const SIDES = ['old', 'new'] as const;
15
+
16
+ interface MountedSplit {
17
+ old: MountedDiff;
18
+ new: MountedDiff;
19
+ align(): void;
20
+ drive(side: DiffSourceSide): void;
21
+ update(run: () => void): void;
22
+ }
23
+
24
+ interface SplitViewProps extends DiffViewProps {
25
+ display: DiffSplit;
26
+ unfold(id: number): void;
27
+ blocked: ReadonlySet<number>;
28
+ }
29
+
30
+ export function SplitDiffView({
31
+ engine,
32
+ document,
33
+ display,
34
+ unfold,
35
+ blocked,
36
+ theme,
37
+ tokens,
38
+ wrap = true,
39
+ fill = false,
40
+ maxHeight,
41
+ label,
42
+ className,
43
+ ref,
44
+ onMount
45
+ }: SplitViewProps) {
46
+ const { t } = useTranslation('editor');
47
+ const emphasis = useMemo(() => wordMarks(document), [document]);
48
+ const oldElement = useRef<HTMLDivElement>(null);
49
+ const newElement = useRef<HTMLDivElement>(null);
50
+ const current = useRef({ display, tokens, theme, wrap, label });
51
+ const applied = useRef({ theme, wrap, label });
52
+ const [failure, setFailure] = useState<{ error: unknown } | null>(null);
53
+ const [mounted, setMounted] = useState<MountedSplit | null>(null);
54
+ const [height, setHeight] = useState(0);
55
+ useLayoutEffect(() => {
56
+ current.current = { display, tokens, theme, wrap, label };
57
+ });
58
+ useLayoutEffect(() => {
59
+ const oldHost = oldElement.current;
60
+ const newHost = newElement.current;
61
+ if (oldHost === null || newHost === null) {
62
+ return;
63
+ }
64
+ const initial = current.current;
65
+ applied.current = { theme: initial.theme, wrap: initial.wrap, label: initial.label };
66
+ const mount = (side: DiffSourceSide, host: HTMLElement): MountedDiff => {
67
+ const initial = current.current;
68
+ const editor = engine.mount(host, {
69
+ text: initial.display[side].text,
70
+ theme: initial.theme,
71
+ wrap: initial.wrap,
72
+ label: initial.label,
73
+ readOnly: true,
74
+ guides: false,
75
+ folding: false,
76
+ foldOutline: 'off',
77
+ handBack: [parseChord('Mod+Enter')]
78
+ });
79
+ editor.setBlocks([]);
80
+ editor.setLineTokens((line) => {
81
+ const row = current.current.display[side].rows[line];
82
+ return row === undefined ? null : current.current.tokens?.get(side, row[side]!.index);
83
+ });
84
+ return { engine, editor, rows: new RowHost(editor, 'diff-alignment'), actions: new LineActionHost(editor, 'diff-status') };
85
+ };
86
+ const old = mount('old', oldHost);
87
+ let fresh: MountedDiff;
88
+ try {
89
+ fresh = mount('new', newHost);
90
+ } catch (error) {
91
+ old.editor.dispose();
92
+ throw error;
93
+ }
94
+ const sides = { old, new: fresh };
95
+ let stopped = false;
96
+ let pending = false;
97
+ let failed = false;
98
+ const scroll = new SplitScroll({ old: old.editor, new: fresh.editor });
99
+ let aligning = false;
100
+ let updating = false;
101
+ const fail = (error: unknown): void => {
102
+ failed = true;
103
+ setFailure({ error });
104
+ };
105
+ const measure = (): void => setHeight(Math.max(sides.old.editor.getContentHeight(), sides.new.editor.getContentHeight()));
106
+ const align = (): void => {
107
+ if (pending || stopped || failed) {
108
+ return;
109
+ }
110
+ pending = true;
111
+ // Intrinsic measurements arrive after each render; align both editors before the next paint.
112
+ queueMicrotask(() => {
113
+ pending = false;
114
+ if (stopped || failed) {
115
+ return;
116
+ }
117
+ aligning = true;
118
+ try {
119
+ const rows = splitAlignment(
120
+ current.current.display.aligned,
121
+ (line) => sides.old.editor.getLineHeight(line),
122
+ (line) => sides.new.editor.getLineHeight(line)
123
+ );
124
+ sides.old.rows.set(rows.old);
125
+ sides.new.rows.set(rows.new);
126
+ } catch (error) {
127
+ fail(error);
128
+ return;
129
+ } finally {
130
+ aligning = false;
131
+ }
132
+ try {
133
+ measure();
134
+ scroll.sync();
135
+ } catch (error) {
136
+ fail(error);
137
+ }
138
+ });
139
+ };
140
+ const update = (run: () => void): void => {
141
+ updating = true;
142
+ try {
143
+ run();
144
+ } finally {
145
+ updating = false;
146
+ }
147
+ align();
148
+ };
149
+ const stops = SIDES.flatMap((side) => [
150
+ sides[side].editor.onLineHeights(align),
151
+ sides[side].editor.onViewChange(() => {
152
+ if (stopped || failed || aligning || updating) {
153
+ return;
154
+ }
155
+ try {
156
+ measure();
157
+ scroll.changed(side);
158
+ } catch (error) {
159
+ fail(error);
160
+ }
161
+ })
162
+ ]);
163
+ setMounted({ old: sides.old, new: sides.new, align, update, drive: (side) => scroll.drive(side) });
164
+ measure();
165
+ return () => {
166
+ stopped = true;
167
+ for (const stop of stops) {
168
+ stop();
169
+ }
170
+ for (const side of SIDES) {
171
+ sides[side].editor.dispose();
172
+ }
173
+ };
174
+ }, [engine]);
175
+ useLayoutEffect(() => {
176
+ if (mounted === null || mounted.old.engine !== engine) {
177
+ return;
178
+ }
179
+ mounted.update(() => {
180
+ for (const side of SIDES) {
181
+ const { editor, actions } = mounted[side];
182
+ editor.setText(display[side].text);
183
+ editor.setLineNumbers((line) => {
184
+ const row = current.current.display[side].rows[line];
185
+ return row === undefined ? null : String(row[side]!.number);
186
+ });
187
+ const highlights: EditorLineHighlight[] = [];
188
+ for (const [index, row] of display[side].rows.entries()) {
189
+ if (row.change === 'context') {
190
+ continue;
191
+ }
192
+ highlights.push({
193
+ startLine: index + 1,
194
+ endLine: index + 1,
195
+ color: side === 'old' ? '--editor-deleted' : '--editor-added',
196
+ bar: side === 'old' ? 'hatched' : 'solid',
197
+ emphasis: [emphasis[side].get(row[side]!.index) ?? []]
198
+ });
199
+ }
200
+ editor.setLineHighlights(highlights);
201
+ editor.setChangeMarks(
202
+ highlights.map((highlight) => ({ startLine: highlight.startLine, endLine: highlight.endLine, kind: side === 'old' ? 'deleted' : 'added' }))
203
+ );
204
+ actions.set(display[side].rows.flatMap((row, line) => (row.noNewline === undefined ? [] : [{ id: `no-newline-${line}`, line }])));
205
+ }
206
+ });
207
+ }, [mounted, engine, display, emphasis]);
208
+ useLayoutEffect(() => {
209
+ if (mounted === null || mounted.old.engine !== engine) {
210
+ return;
211
+ }
212
+ for (const side of SIDES) {
213
+ mounted[side].editor.setLineLabels((line) => {
214
+ const row = display[side].rows[line];
215
+ if (row === undefined || row.change === 'context') {
216
+ return null;
217
+ }
218
+ return t(side === 'old' ? 'diff.removed' : 'diff.added', { number: row[side]!.number });
219
+ });
220
+ }
221
+ }, [mounted, engine, display, t]);
222
+ useLayoutEffect(() => {
223
+ if (mounted === null || mounted.old.engine !== engine) {
224
+ return;
225
+ }
226
+ mounted.update(() => {
227
+ for (const side of SIDES) {
228
+ const previous = applied.current;
229
+ if (previous.theme !== theme) {
230
+ mounted[side].editor.setTheme(theme);
231
+ }
232
+ if (previous.wrap !== wrap) {
233
+ mounted[side].editor.setWrap(wrap);
234
+ }
235
+ if (previous.label !== label) {
236
+ mounted[side].editor.setLabel(label);
237
+ }
238
+ }
239
+ });
240
+ applied.current = { theme, wrap, label };
241
+ }, [mounted, engine, theme, wrap, label]);
242
+ useLayoutEffect(() => {
243
+ if (mounted === null || mounted.old.engine !== engine) {
244
+ return;
245
+ }
246
+ const refresh = (): void => {
247
+ for (const side of SIDES) {
248
+ mounted[side].editor.setLineTokens((line) => {
249
+ const row = current.current.display[side].rows[line];
250
+ return row === undefined ? null : current.current.tokens?.get(side, row[side]!.index);
251
+ });
252
+ }
253
+ };
254
+ refresh();
255
+ const ownerWindow = oldElement.current?.ownerDocument.defaultView;
256
+ let frame: number | undefined;
257
+ let pending = false;
258
+ let stopped = false;
259
+ const schedule = (): void => {
260
+ if (pending) {
261
+ return;
262
+ }
263
+ pending = true;
264
+ const paint = (): void => {
265
+ pending = false;
266
+ if (!stopped) {
267
+ refresh();
268
+ }
269
+ };
270
+ if (ownerWindow?.requestAnimationFrame !== undefined) {
271
+ frame = ownerWindow.requestAnimationFrame(paint);
272
+ } else {
273
+ queueMicrotask(paint);
274
+ }
275
+ };
276
+ const stop = tokens?.subscribe(schedule);
277
+ return () => {
278
+ stopped = true;
279
+ stop?.();
280
+ if (frame !== undefined) {
281
+ ownerWindow?.cancelAnimationFrame?.(frame);
282
+ }
283
+ };
284
+ }, [mounted, engine, display, tokens]);
285
+ useLayoutEffect(() => {
286
+ if (mounted === null || mounted.old.engine !== engine) {
287
+ return;
288
+ }
289
+ const stops = SIDES.map((side) => onMount?.(mounted[side].editor, side));
290
+ return () => {
291
+ for (const stop of stops) {
292
+ stop?.();
293
+ }
294
+ };
295
+ }, [mounted, engine, onMount]);
296
+ const limit = maxHeight !== undefined && Number.isFinite(maxHeight) && maxHeight > 0 ? maxHeight : Infinity;
297
+ if (failure !== null) {
298
+ throw failure.error;
299
+ }
300
+ return (
301
+ <div
302
+ className={`adecore-editor-diff adecore-editor-diff-split ${className ?? ''}`}
303
+ ref={ref}
304
+ role="group"
305
+ aria-label={label}
306
+ data-scroll={fill || height > limit}
307
+ style={{ height: fill ? '100%' : Math.min(height, limit) }}
308
+ >
309
+ {SIDES.map((side) => (
310
+ <div
311
+ key={side}
312
+ className="adecore-editor-diff-side"
313
+ data-side={side}
314
+ role="group"
315
+ aria-label={t(`diff.${side}Side`)}
316
+ onWheelCapture={() => {
317
+ mounted?.drive(side);
318
+ }}
319
+ onFocusCapture={() => {
320
+ mounted?.drive(side);
321
+ }}
322
+ onPointerDownCapture={() => {
323
+ mounted?.drive(side);
324
+ }}
325
+ onKeyDownCapture={() => {
326
+ mounted?.drive(side);
327
+ }}
328
+ >
329
+ <div ref={side === 'old' ? oldElement : newElement} className="adecore-editor-diff-surface" />
330
+ {mounted !== null && mounted[side].engine === engine && (
331
+ <DiffOverlays mounted={mounted[side]} display={display[side]} unfold={unfold} blocked={blocked} />
332
+ )}
333
+ </div>
334
+ ))}
335
+ </div>
336
+ );
337
+ }
@@ -28,7 +28,8 @@ const TONES: Record<SymbolTone, string> = {
28
28
 
29
29
  const SEARCH_DELAY_MS = 200;
30
30
 
31
- function Badge({ kind }: { kind: number }) {
31
+ /* The letter badge of a symbol kind, tinted by what the symbol is. */
32
+ export function SymbolBadge({ kind }: { kind: number }) {
32
33
  return (
33
34
  <span className={`grid size-4 shrink-0 place-items-center rounded-sm font-mono text-xs font-semibold ${TONES[toneOfKind(kind)]}`}>
34
35
  {letterOfKind(kind)}
@@ -153,7 +154,7 @@ export function SymbolPicker({ language, view }: { language: EditorLanguage; vie
153
154
  className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row"
154
155
  onClick={() => choose(index)}
155
156
  >
156
- <Badge kind={place.kind} />
157
+ <SymbolBadge kind={place.kind} />
157
158
  <span className="shrink-0">{place.name}</span>
158
159
  <span className="min-w-0 truncate font-mono text-text-faint">{place.container}</span>
159
160
  <span className="ml-auto shrink-0 font-mono text-text-faint">
@@ -182,7 +183,7 @@ export function SymbolPicker({ language, view }: { language: EditorLanguage; vie
182
183
  className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row"
183
184
  onClick={() => choose(position)}
184
185
  >
185
- <Badge kind={entry.kind} />
186
+ <SymbolBadge kind={entry.kind} />
186
187
  <span className="shrink-0">{entry.name}</span>
187
188
  <span className="min-w-0 truncate font-mono text-text-faint">{entry.detail}</span>
188
189
  {entry.container !== '' && <span className="min-w-0 truncate text-text-faint">{entry.container}</span>}
@@ -0,0 +1,151 @@
1
+ import type { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, Range } from '@adecore/lsp';
2
+
3
+ /* Callers of a symbol, or the symbols it calls. */
4
+ export type CallDirection = 'incoming' | 'outgoing';
5
+
6
+ /* The most calls of one level that are drawn at once; the rest wait behind a row that shows more. */
7
+ export const CALL_LEVEL_LIMIT = 50;
8
+
9
+ export type CallLoad = 'idle' | 'loading' | 'loaded' | 'failed';
10
+
11
+ /* One symbol of the tree and what is known of its calls. */
12
+ export interface CallNode {
13
+ readonly id: string;
14
+ readonly parent: string | null;
15
+ /* One for a root. */
16
+ readonly level: number;
17
+ readonly item: CallHierarchyItem;
18
+ /* Where the call is made, in `siteUri`; empty for a root. */
19
+ readonly sites: readonly Range[];
20
+ readonly siteUri: string | null;
21
+ readonly expanded: boolean;
22
+ readonly load: CallLoad;
23
+ readonly error: string | null;
24
+ /* Every call the server returned, in the order they are listed. */
25
+ readonly children: readonly string[];
26
+ /* How many of the children are drawn. */
27
+ readonly shown: number;
28
+ }
29
+
30
+ /* A line of the tree as it is drawn. */
31
+ export type CallRow =
32
+ | {
33
+ readonly kind: 'node';
34
+ readonly id: string;
35
+ readonly level: number;
36
+ readonly item: CallHierarchyItem;
37
+ readonly expanded: boolean;
38
+ readonly load: CallLoad;
39
+ readonly sites: number;
40
+ /* Whether the server returned calls for it, which only a loaded node knows. */
41
+ readonly leaf: boolean;
42
+ /* Where this row stands among the siblings, from one. */
43
+ readonly position: number;
44
+ readonly size: number;
45
+ }
46
+ | {
47
+ readonly kind: 'status';
48
+ readonly id: string;
49
+ /* The node the status is about, which a failed one loads again. */
50
+ readonly parent: string;
51
+ readonly level: number;
52
+ readonly status: 'loading' | 'empty' | 'failed';
53
+ readonly message: string | null;
54
+ }
55
+ | { readonly kind: 'more'; readonly id: string; readonly level: number; readonly parent: string; readonly hidden: number };
56
+
57
+ /* The id of the status line under a node. */
58
+ export function statusIdOf(parent: string): string {
59
+ return `${parent}:status`;
60
+ }
61
+
62
+ /* The id of the line that shows more calls under a node. */
63
+ export function moreIdOf(parent: string): string {
64
+ return `${parent}:more`;
65
+ }
66
+
67
+ /* The tree's roots, each one a symbol the server prepared. */
68
+ export function rootsOf(items: readonly CallHierarchyItem[]): CallNode[] {
69
+ return items.map((item, index) => ({
70
+ id: `r${index}`,
71
+ parent: null,
72
+ level: 1,
73
+ item,
74
+ sites: [],
75
+ siteUri: null,
76
+ expanded: true,
77
+ load: 'idle',
78
+ error: null,
79
+ children: [],
80
+ shown: 0
81
+ }));
82
+ }
83
+
84
+ /*
85
+ * The children of a node made from the calls a server returned, in the order the server gave them.
86
+ * The call sites of a caller are in its own file; those of a callee are in the file of the node that was asked about.
87
+ */
88
+ export function childrenOf(direction: CallDirection, parent: CallNode, calls: readonly (CallHierarchyIncomingCall | CallHierarchyOutgoingCall)[]): CallNode[] {
89
+ const entries = calls.map((call) => ({ item: 'from' in call ? call.from : call.to, sites: call.fromRanges }));
90
+ return entries.map((entry, index) => ({
91
+ id: `${parent.id}.${index}`,
92
+ parent: parent.id,
93
+ level: parent.level + 1,
94
+ item: entry.item,
95
+ sites: entry.sites,
96
+ siteUri: direction === 'incoming' ? entry.item.uri : parent.item.uri,
97
+ expanded: false,
98
+ load: 'idle',
99
+ error: null,
100
+ children: [],
101
+ shown: 0
102
+ }));
103
+ }
104
+
105
+ /* The lines the tree draws: every root, and under an expanded node its status, its calls and the row that shows more. */
106
+ export function rowsOf(nodes: ReadonlyMap<string, CallNode>, roots: readonly string[]): CallRow[] {
107
+ const rows: CallRow[] = [];
108
+ const visit = (id: string, position: number, size: number): void => {
109
+ const node = nodes.get(id)!;
110
+ rows.push({
111
+ kind: 'node',
112
+ id,
113
+ level: node.level,
114
+ item: node.item,
115
+ expanded: node.expanded,
116
+ load: node.load,
117
+ sites: node.sites.length,
118
+ leaf: node.load === 'loaded' && node.children.length === 0,
119
+ position,
120
+ size
121
+ });
122
+ if (!node.expanded) {
123
+ return;
124
+ }
125
+ if (node.load === 'loading') {
126
+ rows.push({ kind: 'status', id: statusIdOf(id), parent: id, level: node.level + 1, status: 'loading', message: null });
127
+ } else if (node.load === 'failed') {
128
+ rows.push({ kind: 'status', id: statusIdOf(id), parent: id, level: node.level + 1, status: 'failed', message: node.error });
129
+ } else if (node.load === 'loaded' && node.children.length === 0) {
130
+ rows.push({ kind: 'status', id: statusIdOf(id), parent: id, level: node.level + 1, status: 'empty', message: null });
131
+ }
132
+ const shown = node.children.slice(0, node.shown);
133
+ shown.forEach((child, index) => visit(child, index + 1, node.children.length));
134
+ if (node.children.length > node.shown) {
135
+ rows.push({ kind: 'more', id: moreIdOf(id), level: node.level + 1, parent: id, hidden: node.children.length - node.shown });
136
+ }
137
+ };
138
+ roots.forEach((root, index) => visit(root, index + 1, roots.length));
139
+ return rows;
140
+ }
141
+
142
+ /* The rows a key can land on: symbols, and the row that shows more. */
143
+ export function stepsOf(rows: readonly CallRow[]): CallRow[] {
144
+ return rows.filter((row) => row.kind !== 'status');
145
+ }
146
+
147
+ /* The place a node goes to: its first call site, or the symbol itself for a root. */
148
+ export function targetOf(node: CallNode, site = 0): { uri: string; range: Range } {
149
+ const range = node.sites[site];
150
+ return range === undefined || node.siteUri === null ? { uri: node.item.uri, range: node.item.selectionRange } : { uri: node.siteUri, range };
151
+ }