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