@adecore/editor-react 0.39.0 → 0.40.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 (154) hide show
  1. package/dist/CallHierarchyPanel.js +30 -55
  2. package/dist/CodeAuthorsCard.js +0 -1
  3. package/dist/CompletionPopup.js +150 -181
  4. package/dist/DiffView.d.ts +1 -1
  5. package/dist/DiffView.js +2 -29
  6. package/dist/PickPopup.js +54 -80
  7. package/dist/SplitDiffView.js +27 -57
  8. package/dist/SymbolPicker.js +2 -8
  9. package/dist/code-actions-model.js +2 -9
  10. package/dist/code-actions.js +10 -11
  11. package/dist/code-vision.js +2 -1
  12. package/dist/completion-model.js +2 -2
  13. package/dist/completion.js +13 -34
  14. package/dist/definition-link.d.ts +1 -0
  15. package/dist/definition-link.js +22 -18
  16. package/dist/diagnostics-model.d.ts +1 -0
  17. package/dist/diagnostics-model.js +3 -1
  18. package/dist/diagnostics.js +1 -2
  19. package/dist/diff-syntax.d.ts +1 -0
  20. package/dist/diff-syntax.js +31 -0
  21. package/dist/doc-comment-model.d.ts +1 -0
  22. package/dist/doc-comment-model.js +1 -1
  23. package/dist/folding.js +2 -3
  24. package/dist/format-on-type.js +6 -10
  25. package/dist/hover-content.d.ts +0 -2
  26. package/dist/hover-content.js +1 -9
  27. package/dist/inlay-hints.d.ts +0 -1
  28. package/dist/inlay-hints.js +11 -41
  29. package/dist/navigation.js +4 -14
  30. package/dist/peek.js +2 -12
  31. package/dist/popup-focus.d.ts +3 -1
  32. package/dist/popup-focus.js +33 -0
  33. package/dist/recent-choices.d.ts +0 -1
  34. package/dist/recent-choices.js +0 -4
  35. package/dist/rendered-docs.d.ts +1 -0
  36. package/dist/rendered-docs.js +15 -28
  37. package/dist/snippet-field-transforms.d.ts +5 -0
  38. package/dist/snippet-field-transforms.js +27 -16
  39. package/dist/snippet-session.js +8 -20
  40. package/dist/symbol-links.js +3 -10
  41. package/dist/symbol-picker-model.d.ts +1 -0
  42. package/dist/symbol-picker-model.js +7 -0
  43. package/dist/unique.d.ts +10 -0
  44. package/dist/unique.js +15 -0
  45. package/package.json +9 -9
  46. package/src/AnchoredPopup.tsx +2 -0
  47. package/src/BaselineStatus.tsx +1 -0
  48. package/src/CallHierarchyPanel.tsx +16 -6
  49. package/src/ChangeReview.tsx +7 -0
  50. package/src/CodeAuthorsCard.tsx +2 -5
  51. package/src/CompletionPopup.tsx +21 -15
  52. package/src/DiffView.tsx +53 -30
  53. package/src/EditorContextMenu.tsx +7 -0
  54. package/src/EditorView.tsx +28 -0
  55. package/src/FindReplace.tsx +8 -0
  56. package/src/HoverCard.tsx +8 -0
  57. package/src/HoverSections.tsx +11 -0
  58. package/src/LanguagePopups.tsx +1 -0
  59. package/src/PanelResizeHandle.tsx +8 -0
  60. package/src/PathText.tsx +1 -0
  61. package/src/PeekPanel.tsx +13 -0
  62. package/src/PickPopup.tsx +7 -6
  63. package/src/RenameCard.tsx +13 -0
  64. package/src/RenderedDocs.tsx +11 -0
  65. package/src/SplitDiffView.tsx +91 -56
  66. package/src/SymbolPicker.tsx +10 -18
  67. package/src/call-hierarchy-model.ts +10 -0
  68. package/src/call-hierarchy.ts +82 -0
  69. package/src/code-actions-model.ts +21 -9
  70. package/src/code-actions.ts +83 -7
  71. package/src/code-authors.ts +20 -0
  72. package/src/code-vision-model.ts +37 -0
  73. package/src/code-vision.ts +47 -3
  74. package/src/completion-call.ts +19 -0
  75. package/src/completion-model.ts +58 -2
  76. package/src/completion.ts +156 -30
  77. package/src/conflict-model.ts +4 -0
  78. package/src/context-menu-rows.ts +1 -0
  79. package/src/context-menu.ts +7 -0
  80. package/src/definition-link.ts +42 -18
  81. package/src/diagnostics-model.ts +19 -1
  82. package/src/diagnostics.ts +31 -2
  83. package/src/diff-expansion.ts +9 -0
  84. package/src/diff-overlays.tsx +8 -0
  85. package/src/diff-syntax.ts +54 -0
  86. package/src/diff-wordmarks.ts +8 -0
  87. package/src/doc-comment-model.ts +51 -1
  88. package/src/editor-language.ts +11 -0
  89. package/src/find-query.ts +7 -0
  90. package/src/folding.ts +10 -3
  91. package/src/format-on-type.ts +23 -10
  92. package/src/highlight-layers.ts +3 -0
  93. package/src/highlights.ts +18 -0
  94. package/src/history.ts +11 -0
  95. package/src/holder.ts +3 -0
  96. package/src/hover-content.ts +34 -17
  97. package/src/hover.ts +57 -0
  98. package/src/index.ts +4 -0
  99. package/src/inlay-hints.ts +86 -47
  100. package/src/key-priority.ts +2 -0
  101. package/src/line-edits.ts +8 -0
  102. package/src/models.ts +6 -0
  103. package/src/navigation-history.ts +5 -0
  104. package/src/navigation.ts +28 -13
  105. package/src/panel-size.ts +7 -0
  106. package/src/patch-decorations.ts +39 -0
  107. package/src/patch-layout.ts +47 -0
  108. package/src/peek-model.ts +21 -0
  109. package/src/peek.ts +49 -13
  110. package/src/pick.ts +35 -0
  111. package/src/popup-focus.ts +11 -1
  112. package/src/popup-placement.ts +5 -0
  113. package/src/popups.ts +2 -6
  114. package/src/project-language.ts +33 -0
  115. package/src/project-problems.ts +11 -0
  116. package/src/proposal-model.ts +40 -0
  117. package/src/recent-choices.ts +3 -5
  118. package/src/refresher.ts +3 -0
  119. package/src/rename-model.ts +28 -0
  120. package/src/rename.ts +56 -0
  121. package/src/rendered-docs.ts +89 -19
  122. package/src/review-group.ts +4 -0
  123. package/src/row-host.ts +12 -0
  124. package/src/selection-ranges.ts +6 -0
  125. package/src/semantic-delta.ts +7 -0
  126. package/src/semantic-model.ts +11 -0
  127. package/src/semantic-tokens.ts +37 -0
  128. package/src/shortcut-keys.ts +3 -0
  129. package/src/signature-model.ts +7 -0
  130. package/src/signature.ts +27 -0
  131. package/src/snippet-field-transforms.ts +48 -13
  132. package/src/snippet-indent.ts +10 -0
  133. package/src/snippet-range-order.ts +5 -0
  134. package/src/snippet-session.ts +173 -31
  135. package/src/snippet-transform-client.ts +14 -0
  136. package/src/snippet-transform-format.ts +98 -0
  137. package/src/snippet-transform-protocol.ts +5 -0
  138. package/src/snippet-transform-queue.ts +53 -0
  139. package/src/snippet-transform-worker.ts +14 -0
  140. package/src/snippet-variables.ts +9 -0
  141. package/src/snippet.ts +107 -0
  142. package/src/split-alignment.ts +10 -0
  143. package/src/split-scroll.ts +8 -0
  144. package/src/symbol-links.ts +9 -12
  145. package/src/symbol-picker-model.ts +25 -0
  146. package/src/symbol-picker.ts +9 -0
  147. package/src/symbols.ts +21 -0
  148. package/src/testing.ts +7 -0
  149. package/src/timers.ts +5 -0
  150. package/src/unique.ts +23 -0
  151. package/src/use-problem-counts.ts +4 -0
  152. package/src/view-state.ts +9 -0
  153. package/src/word-diff.ts +5 -0
  154. package/src/workspace-edit.ts +84 -0
@@ -8,30 +8,14 @@ import { formatNumber } from '@adecore/ui/format';
8
8
  import type { EditorLanguage } from './editor-language.ts';
9
9
  import { basenameOf } from './paths.ts';
10
10
  import type { SymbolsView } from './popups.ts';
11
- import {
12
- filterEntries,
13
- groupEntries,
14
- letterOfKind,
15
- modeOf,
16
- toneOfKind,
17
- type SymbolEntry,
18
- type SymbolTone,
19
- type WorkspaceEntry
20
- } from './symbol-picker-model.ts';
21
-
22
- const TONES: Record<SymbolTone, string> = {
23
- callable: 'bg-accent-soft text-accent',
24
- value: 'bg-status-running/15 text-status-running',
25
- type: 'bg-status-idle/15 text-status-idle',
26
- other: 'bg-surface-hover text-text-muted'
27
- };
11
+ import { filterEntries, groupEntries, letterOfKind, modeOf, toneOfKind, type SymbolEntry, TONE_CLASSES, type WorkspaceEntry } from './symbol-picker-model.ts';
28
12
 
29
13
  const SEARCH_DELAY_MS = 200;
30
14
 
31
15
  /* The letter badge of a symbol kind, tinted by what the symbol is. */
32
16
  export function SymbolBadge({ kind }: { kind: number }) {
33
17
  return (
34
- <span className={`grid size-4 shrink-0 place-items-center rounded-sm font-mono text-xs font-semibold ${TONES[toneOfKind(kind)]}`}>
18
+ <span className={`grid size-4 shrink-0 place-items-center rounded-sm font-mono text-xs font-semibold ${TONE_CLASSES[toneOfKind(kind)]}`}>
35
19
  {letterOfKind(kind)}
36
20
  </span>
37
21
  );
@@ -60,14 +44,17 @@ export function SymbolPicker({ language, view }: { language: EditorLanguage; vie
60
44
  if (searchText === '') {
61
45
  return;
62
46
  }
47
+
63
48
  const controller = new AbortController();
64
49
  const text = searchText;
50
+
65
51
  const timer = setTimeout(() => {
66
52
  language.symbolPicker
67
53
  .search(text, controller.signal)
68
54
  .then((entries) => !controller.signal.aborted && setFound({ text, entries }))
69
55
  .catch(() => undefined);
70
56
  }, SEARCH_DELAY_MS);
57
+
71
58
  return () => {
72
59
  clearTimeout(timer);
73
60
  controller.abort();
@@ -85,6 +72,7 @@ export function SymbolPicker({ language, view }: { language: EditorLanguage; vie
85
72
  }
86
73
  } else if (mode.mode === 'workspace') {
87
74
  const place = places[index];
75
+
88
76
  if (place !== undefined) {
89
77
  language.symbolPicker.goToWorkspace(place);
90
78
  }
@@ -109,6 +97,7 @@ export function SymbolPicker({ language, view }: { language: EditorLanguage; vie
109
97
  }
110
98
 
111
99
  let offset = 0;
100
+
112
101
  return createPortal(
113
102
  <div
114
103
  className="fixed top-[12vh] left-1/2 z-(--z-popup) flex w-[560px] max-w-[calc(100vw-16px)] -translate-x-1/2 flex-col overflow-hidden rounded-lg border border-border bg-surface-raised text-text shadow-(--float-shadow)"
@@ -143,6 +132,7 @@ export function SymbolPicker({ language, view }: { language: EditorLanguage; vie
143
132
  {mode.mode === 'workspace' &&
144
133
  places.map((place, index) => {
145
134
  const path = fileUriToPath(place.uri);
135
+
146
136
  return (
147
137
  <button
148
138
  key={place.id}
@@ -167,11 +157,13 @@ export function SymbolPicker({ language, view }: { language: EditorLanguage; vie
167
157
  groups.map((group) => {
168
158
  const start = offset;
169
159
  offset += group.entries.length;
160
+
170
161
  return (
171
162
  <div key={group.group} role="group">
172
163
  <div className="px-2 pt-1.5 pb-0.5 text-xs font-medium text-text-faint">{t(`language.symbols.groups.${group.group}`)}</div>
173
164
  {group.entries.map((entry, index) => {
174
165
  const position = start + index;
166
+
175
167
  return (
176
168
  <button
177
169
  key={entry.id}
@@ -87,6 +87,7 @@ export function rootsOf(items: readonly CallHierarchyItem[]): CallNode[] {
87
87
  */
88
88
  export function childrenOf(direction: CallDirection, parent: CallNode, calls: readonly (CallHierarchyIncomingCall | CallHierarchyOutgoingCall)[]): CallNode[] {
89
89
  const entries = calls.map((call) => ({ item: 'from' in call ? call.from : call.to, sites: call.fromRanges }));
90
+
90
91
  return entries.map((entry, index) => ({
91
92
  id: `${parent.id}.${index}`,
92
93
  parent: parent.id,
@@ -105,8 +106,10 @@ export function childrenOf(direction: CallDirection, parent: CallNode, calls: re
105
106
  /* The lines the tree draws: every root, and under an expanded node its status, its calls and the row that shows more. */
106
107
  export function rowsOf(nodes: ReadonlyMap<string, CallNode>, roots: readonly string[]): CallRow[] {
107
108
  const rows: CallRow[] = [];
109
+
108
110
  const visit = (id: string, position: number, size: number): void => {
109
111
  const node = nodes.get(id)!;
112
+
110
113
  rows.push({
111
114
  kind: 'node',
112
115
  id,
@@ -119,9 +122,11 @@ export function rowsOf(nodes: ReadonlyMap<string, CallNode>, roots: readonly str
119
122
  position,
120
123
  size
121
124
  });
125
+
122
126
  if (!node.expanded) {
123
127
  return;
124
128
  }
129
+
125
130
  if (node.load === 'loading') {
126
131
  rows.push({ kind: 'status', id: statusIdOf(id), parent: id, level: node.level + 1, status: 'loading', message: null });
127
132
  } else if (node.load === 'failed') {
@@ -129,13 +134,17 @@ export function rowsOf(nodes: ReadonlyMap<string, CallNode>, roots: readonly str
129
134
  } else if (node.load === 'loaded' && node.children.length === 0) {
130
135
  rows.push({ kind: 'status', id: statusIdOf(id), parent: id, level: node.level + 1, status: 'empty', message: null });
131
136
  }
137
+
132
138
  const shown = node.children.slice(0, node.shown);
133
139
  shown.forEach((child, index) => visit(child, index + 1, node.children.length));
140
+
134
141
  if (node.children.length > node.shown) {
135
142
  rows.push({ kind: 'more', id: moreIdOf(id), level: node.level + 1, parent: id, hidden: node.children.length - node.shown });
136
143
  }
137
144
  };
145
+
138
146
  roots.forEach((root, index) => visit(root, index + 1, roots.length));
147
+
139
148
  return rows;
140
149
  }
141
150
 
@@ -147,5 +156,6 @@ export function stepsOf(rows: readonly CallRow[]): CallRow[] {
147
156
  /* The place a node goes to: its first call site, or the symbol itself for a root. */
148
157
  export function targetOf(node: CallNode, site = 0): { uri: string; range: Range } {
149
158
  const range = node.sites[site];
159
+
150
160
  return range === undefined || node.siteUri === null ? { uri: node.item.uri, range: node.item.selectionRange } : { uri: node.siteUri, range };
151
161
  }
@@ -44,8 +44,10 @@ export class CallHierarchyFeature {
44
44
  private prepared: CallHierarchyItem[] = [];
45
45
  private direction: CallDirection = 'incoming';
46
46
  private controller = new AbortController();
47
+
47
48
  /* Raised whenever the tree starts over or closes, so an answer for the tree that was never lands in the one that is. */
48
49
  private epoch = 0;
50
+
49
51
  private previewEpoch = 0;
50
52
  private texts = new Map<string, string>();
51
53
 
@@ -53,6 +55,7 @@ export class CallHierarchyFeature {
53
55
  this.language = language;
54
56
  this.timers = timers;
55
57
  const { editor } = language;
58
+
56
59
  const offs = [
57
60
  language.onKey((event) => {
58
61
  if (isShortcut(this.language.shortcuts.callHierarchy, event)) {
@@ -61,16 +64,20 @@ export class CallHierarchyFeature {
61
64
  } else {
62
65
  void this.open();
63
66
  }
67
+
64
68
  return true;
65
69
  }
70
+
66
71
  return this.key(event);
67
72
  }, KEY_PRIORITY.callHierarchy),
68
73
  editor.onTextChange(() => this.close())
69
74
  ];
75
+
70
76
  language.onDispose(() => {
71
77
  for (const off of offs) {
72
78
  off();
73
79
  }
80
+
74
81
  this.close();
75
82
  });
76
83
  }
@@ -83,6 +90,7 @@ export class CallHierarchyFeature {
83
90
  get supported(): boolean {
84
91
  const { project, uri } = this.language;
85
92
  const { service } = project;
93
+
86
94
  return (
87
95
  service.prepareCallHierarchy !== undefined &&
88
96
  service.incomingCalls !== undefined &&
@@ -94,43 +102,57 @@ export class CallHierarchyFeature {
94
102
  /* Opens the tree for the symbol at the position, on its callers unless told otherwise. */
95
103
  async open(position: EditorPosition = this.language.editor.getCaret(), direction: CallDirection = 'incoming'): Promise<void> {
96
104
  const { editor, project, uri } = this.language;
105
+
97
106
  if (!this.supported) {
98
107
  this.tell(this.say('unavailable'));
108
+
99
109
  return;
100
110
  }
111
+
101
112
  this.close();
102
113
  const epoch = this.epoch;
103
114
  let items: CallHierarchyItem[] | null;
115
+
104
116
  try {
105
117
  items = await project.service.prepareCallHierarchy!(uri, position, { signal: this.controller.signal });
106
118
  } catch (error) {
107
119
  if (epoch === this.epoch && !(error instanceof StaleResultError)) {
108
120
  this.tell(this.say('failed', { message: messageOf(error) }));
109
121
  }
122
+
110
123
  return;
111
124
  }
125
+
112
126
  if (epoch !== this.epoch) {
113
127
  return;
114
128
  }
129
+
115
130
  if (items === null || items.length === 0) {
116
131
  this.tell(this.say('none'));
132
+
117
133
  return;
118
134
  }
135
+
119
136
  this.language.peek.close();
120
137
  this.prepared = items;
121
138
  this.direction = direction;
122
139
  this.seed();
123
140
  const active = this.roots[0]!;
141
+
124
142
  this.language.popups.setState({
125
143
  callHierarchy: { direction, container: null, name: items[0]!.name, rows: rowsOf(this.nodes, this.roots), active, site: 0, sites: 0, preview: null }
126
144
  });
145
+
127
146
  this.select(active);
147
+
128
148
  // The editor draws a row once per id, so a tree opened over another needs an id of its own to get a container.
129
149
  editor.setWidgets(
130
150
  [{ id: `call-hierarchy-${epoch}`, line: position.line, height: CALL_HIERARCHY_HEIGHT, render: (container) => this.mounted(container) }],
131
151
  OWNER
132
152
  );
153
+
133
154
  editor.focus();
155
+
134
156
  for (const root of this.roots) {
135
157
  void this.load(root);
136
158
  }
@@ -138,9 +160,11 @@ export class CallHierarchyFeature {
138
160
 
139
161
  close(): void {
140
162
  this.reset();
163
+
141
164
  if (this.language.popups.getState().callHierarchy === null) {
142
165
  return;
143
166
  }
167
+
144
168
  this.language.popups.setState({ callHierarchy: null });
145
169
  this.language.editor.setWidgets([], OWNER);
146
170
  this.language.editor.setInputPopup(null);
@@ -150,17 +174,22 @@ export class CallHierarchyFeature {
150
174
  /* Starts the tree over from the same symbols for the other direction. */
151
175
  setDirection(direction: CallDirection): void {
152
176
  const view = this.language.popups.getState().callHierarchy;
177
+
153
178
  if (view === null || view.direction === direction) {
154
179
  return;
155
180
  }
181
+
156
182
  this.reset();
157
183
  this.direction = direction;
158
184
  this.seed();
159
185
  const active = this.roots[0]!;
186
+
160
187
  this.language.popups.setState({
161
188
  callHierarchy: { ...view, direction, rows: rowsOf(this.nodes, this.roots), active, site: 0, sites: 0, preview: null }
162
189
  });
190
+
163
191
  this.select(active);
192
+
164
193
  for (const root of this.roots) {
165
194
  void this.load(root);
166
195
  }
@@ -169,18 +198,24 @@ export class CallHierarchyFeature {
169
198
  /* Opens a node, asking the server for its calls the first time, or folds it away. */
170
199
  toggle(id: string): void {
171
200
  const node = this.nodes.get(id);
201
+
172
202
  if (node === undefined) {
173
203
  return;
174
204
  }
205
+
175
206
  if (node.expanded) {
176
207
  this.patch(id, { expanded: false });
177
208
  const view = this.language.popups.getState().callHierarchy;
209
+
178
210
  if (view !== null && this.hasAncestor(view.active, id)) {
179
211
  this.select(id);
180
212
  }
213
+
181
214
  return;
182
215
  }
216
+
183
217
  this.patch(id, { expanded: true });
218
+
184
219
  if (node.load === 'idle' || node.load === 'failed') {
185
220
  void this.load(id);
186
221
  }
@@ -196,12 +231,15 @@ export class CallHierarchyFeature {
196
231
  /* Brings the next stretch of calls of a level. */
197
232
  showMore(parent: string): void {
198
233
  const node = this.nodes.get(parent);
234
+
199
235
  if (node === undefined) {
200
236
  return;
201
237
  }
238
+
202
239
  this.patch(parent, { shown: Math.min(node.children.length, node.shown + CALL_LEVEL_LIMIT) });
203
240
  const first = node.children[node.shown];
204
241
  const view = this.language.popups.getState().callHierarchy;
242
+
205
243
  // The row that offered more is gone once everything shows, so the selection moves on to what it brought.
206
244
  if (first !== undefined && view?.active === moreIdOf(parent)) {
207
245
  this.select(first);
@@ -212,12 +250,15 @@ export class CallHierarchyFeature {
212
250
  select(id: string, site = 0): void {
213
251
  const view = this.language.popups.getState().callHierarchy;
214
252
  const node = this.nodes.get(id);
253
+
215
254
  if (view === null || !view.rows.some((row) => row.id === id)) {
216
255
  return;
217
256
  }
257
+
218
258
  const sites = node?.sites.length ?? view.sites;
219
259
  const next = node === undefined ? view.site : Math.max(0, Math.min(Math.max(0, sites - 1), site));
220
260
  this.language.popups.setState({ callHierarchy: { ...view, active: id, site: next, sites, preview: node === undefined ? view.preview : null } });
261
+
221
262
  if (node !== undefined) {
222
263
  void this.preview(node, next);
223
264
  }
@@ -226,6 +267,7 @@ export class CallHierarchyFeature {
226
267
  /* Steps through the places a call is made from when the active one has more than one. */
227
268
  stepSite(direction: 1 | -1): void {
228
269
  const view = this.language.popups.getState().callHierarchy;
270
+
229
271
  if (view !== null && view.sites > 1) {
230
272
  this.select(view.active, (view.site + direction + view.sites) % view.sites);
231
273
  }
@@ -235,9 +277,11 @@ export class CallHierarchyFeature {
235
277
  go(id: string): void {
236
278
  const view = this.language.popups.getState().callHierarchy;
237
279
  const node = this.nodes.get(id);
280
+
238
281
  if (view === null || node === undefined) {
239
282
  return;
240
283
  }
284
+
241
285
  const { uri, range } = targetOf(node, id === view.active ? view.site : 0);
242
286
  this.close();
243
287
  this.language.goTo({ uri, range });
@@ -261,6 +305,7 @@ export class CallHierarchyFeature {
261
305
 
262
306
  private mounted(container: HTMLElement): void {
263
307
  const view = this.language.popups.getState().callHierarchy;
308
+
264
309
  if (view !== null && view.container !== container) {
265
310
  this.language.popups.setState({ callHierarchy: { ...view, container } });
266
311
  revealRow(this.timers, container);
@@ -270,43 +315,55 @@ export class CallHierarchyFeature {
270
315
  private patch(id: string, change: Partial<CallNode>): void {
271
316
  const node = this.nodes.get(id);
272
317
  const view = this.language.popups.getState().callHierarchy;
318
+
273
319
  if (node === undefined || view === null) {
274
320
  return;
275
321
  }
322
+
276
323
  this.nodes.set(id, { ...node, ...change });
277
324
  this.language.popups.setState({ callHierarchy: { ...view, rows: rowsOf(this.nodes, this.roots) } });
278
325
  }
279
326
 
280
327
  private async load(id: string): Promise<void> {
281
328
  const node = this.nodes.get(id);
329
+
282
330
  if (node === undefined || node.load === 'loading') {
283
331
  return;
284
332
  }
333
+
285
334
  const { service, i18n } = this.language.project;
286
335
  const { uri, editor } = this.language;
287
336
  const { direction, epoch } = this;
288
337
  const options = { signal: this.controller.signal, parallel: true };
289
338
  this.patch(id, { load: 'loading', error: null });
339
+
290
340
  try {
291
341
  const calls =
292
342
  direction === 'incoming' ? await service.incomingCalls!(uri, node.item, options) : await service.outgoingCalls!(uri, node.item, options);
343
+
293
344
  if (epoch !== this.epoch) {
294
345
  return;
295
346
  }
347
+
296
348
  const children = childrenOf(direction, node, calls ?? []);
349
+
297
350
  for (const child of children) {
298
351
  this.nodes.set(child.id, child);
299
352
  }
353
+
300
354
  this.patch(id, { load: 'loaded', children: children.map((child) => child.id), shown: Math.min(children.length, CALL_LEVEL_LIMIT) });
301
355
  editor.announce(i18n.t(`editor:language.callHierarchy.announce.${direction}`, { count: children.length, name: node.item.name }));
302
356
  } catch (error) {
303
357
  if (epoch !== this.epoch) {
304
358
  return;
305
359
  }
360
+
306
361
  if (error instanceof StaleResultError) {
307
362
  this.close();
363
+
308
364
  return;
309
365
  }
366
+
310
367
  this.patch(id, { load: 'failed', error: messageOf(error) });
311
368
  }
312
369
  }
@@ -316,6 +373,7 @@ export class CallHierarchyFeature {
316
373
  const epoch = ++this.previewEpoch;
317
374
  const { uri, range } = targetOf(node, site);
318
375
  let text = this.texts.get(uri);
376
+
319
377
  if (text === undefined) {
320
378
  try {
321
379
  text = (await project.readText(uri)) ?? undefined;
@@ -323,17 +381,22 @@ export class CallHierarchyFeature {
323
381
  // A file that cannot be read shows no code, as one that is no text file does.
324
382
  text = undefined;
325
383
  }
384
+
326
385
  if (epoch !== this.previewEpoch) {
327
386
  return;
328
387
  }
388
+
329
389
  if (text !== undefined) {
330
390
  this.texts.set(uri, text);
331
391
  }
332
392
  }
393
+
333
394
  const view = this.language.popups.getState().callHierarchy;
395
+
334
396
  if (view === null || view.active !== node.id) {
335
397
  return;
336
398
  }
399
+
337
400
  const snippet = text === undefined ? null : snippetOf(text, range.start.line, range);
338
401
  this.language.popups.setState({ callHierarchy: { ...view, preview: snippet === null ? null : { ...snippet, uri } } });
339
402
  }
@@ -343,43 +406,55 @@ export class CallHierarchyFeature {
343
406
  const owner = [moreIdOf(''), statusIdOf('')]
344
407
  .map((suffix) => (id.endsWith(suffix) ? id.slice(0, -suffix.length) : null))
345
408
  .find((found) => found !== null);
409
+
346
410
  if (owner !== undefined && owner !== null) {
347
411
  return owner === ancestor || this.hasAncestor(owner, ancestor);
348
412
  }
413
+
349
414
  for (let node = this.nodes.get(id); node !== undefined && node.parent !== null; node = this.nodes.get(node.parent)) {
350
415
  if (node.parent === ancestor) {
351
416
  return true;
352
417
  }
353
418
  }
419
+
354
420
  return false;
355
421
  }
356
422
 
357
423
  private key(event: KeyboardEvent): boolean {
358
424
  const view = this.language.popups.getState().callHierarchy;
425
+
359
426
  if (view === null || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.isComposing) {
360
427
  return false;
361
428
  }
429
+
362
430
  const steps = stepsOf(view.rows);
363
431
  const index = steps.findIndex((row) => row.id === view.active);
364
432
  const row: CallRow | undefined = steps[index];
433
+
365
434
  switch (event.key) {
366
435
  case 'ArrowDown':
367
436
  this.land(steps[Math.min(steps.length - 1, index + 1)]);
437
+
368
438
  return true;
369
439
  case 'ArrowUp':
370
440
  this.land(steps[Math.max(0, index - 1)]);
441
+
371
442
  return true;
372
443
  case 'Home':
373
444
  this.land(steps[0]);
445
+
374
446
  return true;
375
447
  case 'End':
376
448
  this.land(steps.at(-1));
449
+
377
450
  return true;
378
451
  case 'ArrowRight':
379
452
  this.expandOrEnter(row, steps[index + 1]);
453
+
380
454
  return true;
381
455
  case 'ArrowLeft':
382
456
  this.collapseOrLeave(row);
457
+
383
458
  return true;
384
459
  case 'Enter':
385
460
  if (row?.kind === 'more') {
@@ -387,9 +462,11 @@ export class CallHierarchyFeature {
387
462
  } else if (row !== undefined) {
388
463
  this.go(row.id);
389
464
  }
465
+
390
466
  return true;
391
467
  case 'Escape':
392
468
  this.close();
469
+
393
470
  return true;
394
471
  default:
395
472
  return false;
@@ -407,6 +484,7 @@ export class CallHierarchyFeature {
407
484
  if (row?.kind !== 'node') {
408
485
  return;
409
486
  }
487
+
410
488
  if (!row.expanded) {
411
489
  this.toggle(row.id);
412
490
  } else if (row.load === 'failed') {
@@ -421,11 +499,15 @@ export class CallHierarchyFeature {
421
499
  if (row === undefined) {
422
500
  return;
423
501
  }
502
+
424
503
  if (row.kind === 'node' && row.expanded && !row.leaf) {
425
504
  this.toggle(row.id);
505
+
426
506
  return;
427
507
  }
508
+
428
509
  const parent = row.kind === 'node' ? (this.nodes.get(row.id)?.parent ?? null) : row.parent;
510
+
429
511
  if (parent !== null) {
430
512
  this.select(parent);
431
513
  }
@@ -1,5 +1,6 @@
1
1
  import { applyTextEdits, type CodeAction, type Command, type Diagnostic, type Range, type WorkspaceEdit } from '@adecore/lsp';
2
2
  import { comparePositions } from './diagnostics-model.ts';
3
+ import { uniqueBy } from './unique.ts';
3
4
  import { resourceOperationsOf, textEntriesOf } from './workspace-edit.ts';
4
5
 
5
6
  export type ActionGroup = 'quickfix' | 'rewrite' | 'extract' | 'inline' | 'move' | 'refactor' | 'source' | 'other';
@@ -21,6 +22,7 @@ export interface ActionEntry {
21
22
  function inGroupOrder(entries: readonly ActionEntry[], order: readonly ActionGroup[]): ActionEntry[] {
22
23
  return order.flatMap((group) => {
23
24
  const own = entries.filter((entry) => entry.group === group);
25
+
24
26
  return [...own.filter((entry) => entry.action.isPreferred === true), ...own.filter((entry) => entry.action.isPreferred !== true)];
25
27
  });
26
28
  }
@@ -30,17 +32,21 @@ export function groupOf(kind: string | undefined): ActionGroup {
30
32
  if (kind === 'quickfix' || kind?.startsWith('quickfix.')) {
31
33
  return 'quickfix';
32
34
  }
35
+
33
36
  for (const group of ['extract', 'inline', 'move', 'rewrite'] as const) {
34
37
  if (kind === `refactor.${group}` || kind?.startsWith(`refactor.${group}.`)) {
35
38
  return group;
36
39
  }
37
40
  }
41
+
38
42
  if (kind === 'refactor' || kind?.startsWith('refactor.')) {
39
43
  return 'refactor';
40
44
  }
45
+
41
46
  if (kind === 'source' || kind?.startsWith('source.')) {
42
47
  return 'source';
43
48
  }
49
+
44
50
  return 'other';
45
51
  }
46
52
 
@@ -53,20 +59,14 @@ export function actionsOf(result: readonly (CodeAction | Command)[] | null, orde
53
59
  .map((item) => (typeof item.command === 'string' ? ({ title: item.title, command: item as Command } satisfies CodeAction) : (item as CodeAction)))
54
60
  .filter((action) => action.disabled === undefined)
55
61
  .map((action, index) => ({ id: String(index), action, group: groupOf(action.kind) }));
62
+
56
63
  return inGroupOrder(entries, order);
57
64
  }
58
65
 
59
66
  /* Entries from several requests as one list: the same order as a single answer, and an id of its own for each row. Two actions with a title and kind alike are one. */
60
67
  export function mergeEntries(lists: readonly (readonly ActionEntry[])[]): ActionEntry[] {
61
- const seen = new Set<string>();
62
- const unique = lists.flat().filter((entry) => {
63
- const key = `${entry.action.kind ?? ''}\0${entry.action.title}`;
64
- if (seen.has(key)) {
65
- return false;
66
- }
67
- seen.add(key);
68
- return true;
69
- });
68
+ const unique = uniqueBy(lists.flat(), (entry) => `${entry.action.kind ?? ''}\0${entry.action.title}`);
69
+
70
70
  return inGroupOrder(unique, ACTION_GROUPS).map((entry, index) => ({ ...entry, id: String(index) }));
71
71
  }
72
72
 
@@ -117,7 +117,9 @@ function linesOf(text: string): string[] {
117
117
  export function previewOf(text: string, edit: WorkspaceEdit, uri: string): EditPreview | null {
118
118
  const entries = textEntriesOf(edit);
119
119
  const operations = resourceOperationsOf(edit);
120
+
120
121
  const count = (kind: string): number => operations.filter((operation) => operation.kind === kind).length;
122
+
121
123
  const moves = count('rename');
122
124
  const creates = count('create');
123
125
  const deletes = count('delete');
@@ -126,29 +128,39 @@ export function previewOf(text: string, edit: WorkspaceEdit, uri: string): EditP
126
128
  const otherFiles = [...entries.keys()].filter((key) => key !== uri && !made.has(key)).length;
127
129
  let removed: string[] = [];
128
130
  let added: string[] = [];
131
+
129
132
  if (own !== undefined) {
130
133
  let after: string;
134
+
131
135
  try {
132
136
  after = applyTextEdits(text, own.flat());
133
137
  } catch {
134
138
  return null;
135
139
  }
140
+
136
141
  const before = linesOf(text);
137
142
  const next = linesOf(after);
138
143
  let head = 0;
144
+
139
145
  while (head < before.length && head < next.length && before[head] === next[head]) {
140
146
  head++;
141
147
  }
148
+
142
149
  let tail = 0;
150
+
143
151
  while (tail < before.length - head && tail < next.length - head && before[before.length - 1 - tail] === next[next.length - 1 - tail]) {
144
152
  tail++;
145
153
  }
154
+
146
155
  removed = before.slice(head, before.length - tail);
147
156
  added = next.slice(head, next.length - tail);
148
157
  }
158
+
149
159
  if (removed.length === 0 && added.length === 0 && otherFiles === 0 && moves === 0 && creates === 0 && deletes === 0) {
150
160
  return null;
151
161
  }
162
+
152
163
  const hiddenLines = Math.max(0, removed.length - PREVIEW_LINES) + Math.max(0, added.length - PREVIEW_LINES);
164
+
153
165
  return { removed: removed.slice(0, PREVIEW_LINES), added: added.slice(0, PREVIEW_LINES), hiddenLines, otherFiles, moves, creates, deletes };
154
166
  }