@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.
- package/dist/CallHierarchyPanel.js +30 -55
- package/dist/CodeAuthorsCard.js +0 -1
- package/dist/CompletionPopup.js +150 -181
- package/dist/DiffView.d.ts +1 -1
- package/dist/DiffView.js +2 -29
- package/dist/PickPopup.js +54 -80
- package/dist/SplitDiffView.js +27 -57
- package/dist/SymbolPicker.js +2 -8
- package/dist/code-actions-model.js +2 -9
- package/dist/code-actions.js +10 -11
- package/dist/code-vision.js +2 -1
- package/dist/completion-model.js +2 -2
- package/dist/completion.js +13 -34
- package/dist/definition-link.d.ts +1 -0
- package/dist/definition-link.js +22 -18
- package/dist/diagnostics-model.d.ts +1 -0
- package/dist/diagnostics-model.js +3 -1
- package/dist/diagnostics.js +1 -2
- package/dist/diff-syntax.d.ts +1 -0
- package/dist/diff-syntax.js +31 -0
- package/dist/doc-comment-model.d.ts +1 -0
- package/dist/doc-comment-model.js +1 -1
- package/dist/folding.js +2 -3
- package/dist/format-on-type.js +6 -10
- package/dist/hover-content.d.ts +0 -2
- package/dist/hover-content.js +1 -9
- package/dist/inlay-hints.d.ts +0 -1
- package/dist/inlay-hints.js +11 -41
- package/dist/navigation.js +4 -14
- package/dist/peek.js +2 -12
- package/dist/popup-focus.d.ts +3 -1
- package/dist/popup-focus.js +33 -0
- package/dist/recent-choices.d.ts +0 -1
- package/dist/recent-choices.js +0 -4
- package/dist/rendered-docs.d.ts +1 -0
- package/dist/rendered-docs.js +15 -28
- package/dist/snippet-field-transforms.d.ts +5 -0
- package/dist/snippet-field-transforms.js +27 -16
- package/dist/snippet-session.js +8 -20
- package/dist/symbol-links.js +3 -10
- package/dist/symbol-picker-model.d.ts +1 -0
- package/dist/symbol-picker-model.js +7 -0
- package/dist/unique.d.ts +10 -0
- package/dist/unique.js +15 -0
- package/package.json +9 -9
- package/src/AnchoredPopup.tsx +2 -0
- package/src/BaselineStatus.tsx +1 -0
- package/src/CallHierarchyPanel.tsx +16 -6
- package/src/ChangeReview.tsx +7 -0
- package/src/CodeAuthorsCard.tsx +2 -5
- package/src/CompletionPopup.tsx +21 -15
- package/src/DiffView.tsx +53 -30
- package/src/EditorContextMenu.tsx +7 -0
- package/src/EditorView.tsx +28 -0
- package/src/FindReplace.tsx +8 -0
- package/src/HoverCard.tsx +8 -0
- package/src/HoverSections.tsx +11 -0
- package/src/LanguagePopups.tsx +1 -0
- package/src/PanelResizeHandle.tsx +8 -0
- package/src/PathText.tsx +1 -0
- package/src/PeekPanel.tsx +13 -0
- package/src/PickPopup.tsx +7 -6
- package/src/RenameCard.tsx +13 -0
- package/src/RenderedDocs.tsx +11 -0
- package/src/SplitDiffView.tsx +91 -56
- package/src/SymbolPicker.tsx +10 -18
- package/src/call-hierarchy-model.ts +10 -0
- package/src/call-hierarchy.ts +82 -0
- package/src/code-actions-model.ts +21 -9
- package/src/code-actions.ts +83 -7
- package/src/code-authors.ts +20 -0
- package/src/code-vision-model.ts +37 -0
- package/src/code-vision.ts +47 -3
- package/src/completion-call.ts +19 -0
- package/src/completion-model.ts +58 -2
- package/src/completion.ts +156 -30
- package/src/conflict-model.ts +4 -0
- package/src/context-menu-rows.ts +1 -0
- package/src/context-menu.ts +7 -0
- package/src/definition-link.ts +42 -18
- package/src/diagnostics-model.ts +19 -1
- package/src/diagnostics.ts +31 -2
- package/src/diff-expansion.ts +9 -0
- package/src/diff-overlays.tsx +8 -0
- package/src/diff-syntax.ts +54 -0
- package/src/diff-wordmarks.ts +8 -0
- package/src/doc-comment-model.ts +51 -1
- package/src/editor-language.ts +11 -0
- package/src/find-query.ts +7 -0
- package/src/folding.ts +10 -3
- package/src/format-on-type.ts +23 -10
- package/src/highlight-layers.ts +3 -0
- package/src/highlights.ts +18 -0
- package/src/history.ts +11 -0
- package/src/holder.ts +3 -0
- package/src/hover-content.ts +34 -17
- package/src/hover.ts +57 -0
- package/src/index.ts +4 -0
- package/src/inlay-hints.ts +86 -47
- package/src/key-priority.ts +2 -0
- package/src/line-edits.ts +8 -0
- package/src/models.ts +6 -0
- package/src/navigation-history.ts +5 -0
- package/src/navigation.ts +28 -13
- package/src/panel-size.ts +7 -0
- package/src/patch-decorations.ts +39 -0
- package/src/patch-layout.ts +47 -0
- package/src/peek-model.ts +21 -0
- package/src/peek.ts +49 -13
- package/src/pick.ts +35 -0
- package/src/popup-focus.ts +11 -1
- package/src/popup-placement.ts +5 -0
- package/src/popups.ts +2 -6
- package/src/project-language.ts +33 -0
- package/src/project-problems.ts +11 -0
- package/src/proposal-model.ts +40 -0
- package/src/recent-choices.ts +3 -5
- package/src/refresher.ts +3 -0
- package/src/rename-model.ts +28 -0
- package/src/rename.ts +56 -0
- package/src/rendered-docs.ts +89 -19
- package/src/review-group.ts +4 -0
- package/src/row-host.ts +12 -0
- package/src/selection-ranges.ts +6 -0
- package/src/semantic-delta.ts +7 -0
- package/src/semantic-model.ts +11 -0
- package/src/semantic-tokens.ts +37 -0
- package/src/shortcut-keys.ts +3 -0
- package/src/signature-model.ts +7 -0
- package/src/signature.ts +27 -0
- package/src/snippet-field-transforms.ts +48 -13
- package/src/snippet-indent.ts +10 -0
- package/src/snippet-range-order.ts +5 -0
- package/src/snippet-session.ts +173 -31
- package/src/snippet-transform-client.ts +14 -0
- package/src/snippet-transform-format.ts +98 -0
- package/src/snippet-transform-protocol.ts +5 -0
- package/src/snippet-transform-queue.ts +53 -0
- package/src/snippet-transform-worker.ts +14 -0
- package/src/snippet-variables.ts +9 -0
- package/src/snippet.ts +107 -0
- package/src/split-alignment.ts +10 -0
- package/src/split-scroll.ts +8 -0
- package/src/symbol-links.ts +9 -12
- package/src/symbol-picker-model.ts +25 -0
- package/src/symbol-picker.ts +9 -0
- package/src/symbols.ts +21 -0
- package/src/testing.ts +7 -0
- package/src/timers.ts +5 -0
- package/src/unique.ts +23 -0
- package/src/use-problem-counts.ts +4 -0
- package/src/view-state.ts +9 -0
- package/src/word-diff.ts +5 -0
- package/src/workspace-edit.ts +84 -0
package/src/peek.ts
CHANGED
|
@@ -8,8 +8,10 @@ import type { PeekView } from './popups.ts';
|
|
|
8
8
|
import { isShortcut } from './shortcut-keys.ts';
|
|
9
9
|
import { messageOf } from './error-message.ts';
|
|
10
10
|
import { realTimers, revealRow, type Timers } from './timers.ts';
|
|
11
|
+
import { placeKey, uniqueBy } from './unique.ts';
|
|
11
12
|
|
|
12
13
|
const TOAST_ID = 'language-peek';
|
|
14
|
+
|
|
13
15
|
/* The block as the panel draws it: a hatched band, the header, eight lines of code and a hatched band, until the editor has measured it. */
|
|
14
16
|
export const PEEK_HEIGHT = 241;
|
|
15
17
|
|
|
@@ -20,10 +22,6 @@ const KINDS = {
|
|
|
20
22
|
definitions: { method: 'textDocument/definition', unavailable: 'unavailableDefinition', none: 'noDefinition' }
|
|
21
23
|
} as const;
|
|
22
24
|
|
|
23
|
-
function placeKey(location: Location): string {
|
|
24
|
-
return `${location.uri}\0${location.range.start.line}\0${location.range.start.character}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
25
|
/*
|
|
28
26
|
* The references of the name at the caret, between the lines of the file under the line it is on. The
|
|
29
27
|
* editor keeps the keyboard: arrows pick a reference, Enter goes to it and Escape closes the peek; typing
|
|
@@ -33,8 +31,10 @@ export class PeekFeature {
|
|
|
33
31
|
private readonly language: EditorLanguage;
|
|
34
32
|
private readonly timers: Timers;
|
|
35
33
|
private kind: PeekKind = 'references';
|
|
34
|
+
|
|
36
35
|
/* The whole declaration of each place of a definition peek, where the server gave it. */
|
|
37
36
|
private targets = new Map<string, Range>();
|
|
37
|
+
|
|
38
38
|
private texts = new Map<string, string>();
|
|
39
39
|
private places = new Map<string, Location>();
|
|
40
40
|
private order: string[] = [];
|
|
@@ -45,24 +45,31 @@ export class PeekFeature {
|
|
|
45
45
|
this.language = language;
|
|
46
46
|
this.timers = timers;
|
|
47
47
|
const { editor } = language;
|
|
48
|
+
|
|
48
49
|
const offs = [
|
|
49
50
|
language.onKey((event) => {
|
|
50
51
|
if (isShortcut(this.language.shortcuts.peekReferences, event)) {
|
|
51
52
|
void this.open();
|
|
53
|
+
|
|
52
54
|
return true;
|
|
53
55
|
}
|
|
56
|
+
|
|
54
57
|
if (isShortcut(this.language.shortcuts.peekDefinition, event)) {
|
|
55
58
|
void this.openDefinition();
|
|
59
|
+
|
|
56
60
|
return true;
|
|
57
61
|
}
|
|
62
|
+
|
|
58
63
|
return this.key(event);
|
|
59
64
|
}, KEY_PRIORITY.peek),
|
|
60
65
|
editor.onTextChange(() => this.close())
|
|
61
66
|
];
|
|
67
|
+
|
|
62
68
|
language.onDispose(() => {
|
|
63
69
|
for (const off of offs) {
|
|
64
70
|
off();
|
|
65
71
|
}
|
|
72
|
+
|
|
66
73
|
this.close();
|
|
67
74
|
});
|
|
68
75
|
}
|
|
@@ -74,61 +81,69 @@ export class PeekFeature {
|
|
|
74
81
|
async open(position: EditorPosition = this.language.editor.getCaret(), kind: PeekKind = 'references'): Promise<void> {
|
|
75
82
|
const { editor, project, uri } = this.language;
|
|
76
83
|
const method = KINDS[kind].method;
|
|
84
|
+
|
|
77
85
|
if (!project.service.supports(method, uri)) {
|
|
78
86
|
this.tell(this.say(KINDS[kind].unavailable));
|
|
87
|
+
|
|
79
88
|
return;
|
|
80
89
|
}
|
|
90
|
+
|
|
81
91
|
const token = ++this.token;
|
|
82
92
|
let found: { locations: Location[]; targets: Map<string, Range> };
|
|
93
|
+
|
|
83
94
|
try {
|
|
84
95
|
found = await this.fetch(kind, position);
|
|
85
96
|
} catch (error) {
|
|
86
97
|
if (!(error instanceof StaleResultError)) {
|
|
87
98
|
this.tell(this.say('failed', { message: messageOf(error) }));
|
|
88
99
|
}
|
|
100
|
+
|
|
89
101
|
return;
|
|
90
102
|
}
|
|
103
|
+
|
|
91
104
|
if (token !== this.token) {
|
|
92
105
|
return;
|
|
93
106
|
}
|
|
107
|
+
|
|
94
108
|
const { targets } = found;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const key = placeKey(location);
|
|
98
|
-
if (seen.has(key)) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
seen.add(key);
|
|
102
|
-
return true;
|
|
103
|
-
});
|
|
109
|
+
const unique = uniqueBy(found.locations, placeKey);
|
|
110
|
+
|
|
104
111
|
if (unique.length === 0) {
|
|
105
112
|
this.tell(this.say(KINDS[kind].none));
|
|
113
|
+
|
|
106
114
|
return;
|
|
107
115
|
}
|
|
116
|
+
|
|
108
117
|
const uris = [...new Set(unique.map((location) => location.uri))].slice(0, PEEK_READ_FILES);
|
|
109
118
|
const texts = new Map<string, string>();
|
|
119
|
+
|
|
110
120
|
await Promise.all(
|
|
111
121
|
uris.map(async (target) => {
|
|
112
122
|
const text = await project.readText(target);
|
|
123
|
+
|
|
113
124
|
if (text !== null) {
|
|
114
125
|
texts.set(target, text);
|
|
115
126
|
}
|
|
116
127
|
})
|
|
117
128
|
);
|
|
129
|
+
|
|
118
130
|
if (token !== this.token) {
|
|
119
131
|
return;
|
|
120
132
|
}
|
|
133
|
+
|
|
121
134
|
this.texts = texts;
|
|
122
135
|
this.kind = kind;
|
|
123
136
|
this.targets = targets;
|
|
124
137
|
const files = peekFilesOf(unique, uri, (target) => texts.get(target) ?? null);
|
|
125
138
|
this.places = new Map(files.flatMap((file: PeekFile) => file.places.map((place) => [place.id, place.location] as const)));
|
|
126
139
|
this.order = files.flatMap((file) => file.places.map((place) => place.id));
|
|
140
|
+
|
|
127
141
|
// The references open on the first place that is not the one the caret is on, which is what a person came to see.
|
|
128
142
|
const first =
|
|
129
143
|
kind === 'definitions'
|
|
130
144
|
? files[0]!.places[0]!
|
|
131
145
|
: (files.flatMap((file) => file.places).find((place) => !(place.location.uri === uri && place.line === position.line)) ?? files[0]!.places[0]!);
|
|
146
|
+
|
|
132
147
|
this.line = position.line;
|
|
133
148
|
this.language.callHierarchy.close();
|
|
134
149
|
this.language.popups.setState({ peek: { kind, container: null, files, active: first.id, collapsed: [], count: unique.length, preview: null } });
|
|
@@ -146,20 +161,25 @@ export class PeekFeature {
|
|
|
146
161
|
|
|
147
162
|
private async fetch(kind: PeekKind, position: EditorPosition): Promise<{ locations: Location[]; targets: Map<string, Range> }> {
|
|
148
163
|
const { project, uri } = this.language;
|
|
164
|
+
|
|
149
165
|
if (kind === 'references') {
|
|
150
166
|
// Without the declaration, so the list holds what the usages count above a declaration counts.
|
|
151
167
|
return { locations: (await project.service.references(uri, position, false)) ?? [], targets: new Map() };
|
|
152
168
|
}
|
|
169
|
+
|
|
153
170
|
const places = uniquePlaces(await project.service.definition(uri, position));
|
|
154
171
|
const targets = new Map(places.flatMap(({ location, target }) => (target === null ? [] : [[placeKey(location), target] as const])));
|
|
172
|
+
|
|
155
173
|
return { locations: places.map(({ location }) => location), targets };
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
close(): void {
|
|
159
177
|
this.token++;
|
|
178
|
+
|
|
160
179
|
if (this.language.popups.getState().peek === null) {
|
|
161
180
|
return;
|
|
162
181
|
}
|
|
182
|
+
|
|
163
183
|
this.language.popups.setState({ peek: null });
|
|
164
184
|
this.language.editor.setWidgets([]);
|
|
165
185
|
this.language.editor.focus();
|
|
@@ -169,23 +189,28 @@ export class PeekFeature {
|
|
|
169
189
|
select(id: string): void {
|
|
170
190
|
const view = this.language.popups.getState().peek;
|
|
171
191
|
const location = this.places.get(id);
|
|
192
|
+
|
|
172
193
|
if (view === null || location === undefined) {
|
|
173
194
|
return;
|
|
174
195
|
}
|
|
196
|
+
|
|
175
197
|
const text = this.texts.get(location.uri);
|
|
176
198
|
const line = location.range.start.line;
|
|
199
|
+
|
|
177
200
|
const snippet =
|
|
178
201
|
text === undefined
|
|
179
202
|
? null
|
|
180
203
|
: this.kind === 'definitions'
|
|
181
204
|
? definitionSnippetOf(text, line, this.targets.get(placeKey(location)) ?? null, location.range)
|
|
182
205
|
: snippetOf(text, line, location.range);
|
|
206
|
+
|
|
183
207
|
this.language.popups.setState({ peek: { ...view, active: id, preview: snippet === null ? null : { ...snippet, uri: location.uri } } });
|
|
184
208
|
}
|
|
185
209
|
|
|
186
210
|
/* Folds the places of a file away from the list, or brings them back. */
|
|
187
211
|
toggleFile(uri: string): void {
|
|
188
212
|
const view = this.language.popups.getState().peek;
|
|
213
|
+
|
|
189
214
|
if (view !== null) {
|
|
190
215
|
const collapsed = view.collapsed.includes(uri) ? view.collapsed.filter((other) => other !== uri) : [...view.collapsed, uri];
|
|
191
216
|
this.language.popups.setState({ peek: { ...view, collapsed } });
|
|
@@ -195,6 +220,7 @@ export class PeekFeature {
|
|
|
195
220
|
/* Goes to a place and closes the peek. */
|
|
196
221
|
go(id: string): void {
|
|
197
222
|
const location = this.places.get(id);
|
|
223
|
+
|
|
198
224
|
if (location !== undefined) {
|
|
199
225
|
this.close();
|
|
200
226
|
this.language.goTo(location);
|
|
@@ -203,6 +229,7 @@ export class PeekFeature {
|
|
|
203
229
|
|
|
204
230
|
private mounted(container: HTMLElement): void {
|
|
205
231
|
const view = this.language.popups.getState().peek;
|
|
232
|
+
|
|
206
233
|
if (view !== null && view.container !== container) {
|
|
207
234
|
this.language.popups.setState({ peek: { ...view, container } });
|
|
208
235
|
revealRow(this.timers, container);
|
|
@@ -211,21 +238,27 @@ export class PeekFeature {
|
|
|
211
238
|
|
|
212
239
|
private key(event: KeyboardEvent): boolean {
|
|
213
240
|
const view = this.language.popups.getState().peek;
|
|
241
|
+
|
|
214
242
|
if (view === null || event.metaKey || event.ctrlKey || event.altKey) {
|
|
215
243
|
return false;
|
|
216
244
|
}
|
|
245
|
+
|
|
217
246
|
switch (event.key) {
|
|
218
247
|
case 'ArrowDown':
|
|
219
248
|
this.step(view, 1);
|
|
249
|
+
|
|
220
250
|
return true;
|
|
221
251
|
case 'ArrowUp':
|
|
222
252
|
this.step(view, -1);
|
|
253
|
+
|
|
223
254
|
return true;
|
|
224
255
|
case 'Enter':
|
|
225
256
|
this.go(view.active);
|
|
257
|
+
|
|
226
258
|
return true;
|
|
227
259
|
case 'Escape':
|
|
228
260
|
this.close();
|
|
261
|
+
|
|
229
262
|
return true;
|
|
230
263
|
default:
|
|
231
264
|
return false;
|
|
@@ -235,10 +268,13 @@ export class PeekFeature {
|
|
|
235
268
|
/* The next place in the list a person can see, which a folded file has none of. */
|
|
236
269
|
private step(view: PeekView, direction: 1 | -1): void {
|
|
237
270
|
const index = this.order.indexOf(view.active);
|
|
271
|
+
|
|
238
272
|
for (let next = index + direction; next >= 0 && next < this.order.length; next += direction) {
|
|
239
273
|
const id = this.order[next]!;
|
|
274
|
+
|
|
240
275
|
if (!view.collapsed.includes(this.places.get(id)!.uri)) {
|
|
241
276
|
this.select(id);
|
|
277
|
+
|
|
242
278
|
return;
|
|
243
279
|
}
|
|
244
280
|
}
|
package/src/pick.ts
CHANGED
|
@@ -37,24 +37,29 @@ export class PickFeature {
|
|
|
37
37
|
private active = '';
|
|
38
38
|
private previewTimer: unknown;
|
|
39
39
|
private controller: AbortController | null = null;
|
|
40
|
+
|
|
40
41
|
/* The rows that said why they cannot be taken, by id. */
|
|
41
42
|
private refusals = new Map<string, string>();
|
|
43
|
+
|
|
42
44
|
private attempting = false;
|
|
43
45
|
|
|
44
46
|
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
45
47
|
this.language = language;
|
|
46
48
|
this.timers = timers;
|
|
47
49
|
const { editor } = language;
|
|
50
|
+
|
|
48
51
|
const offs = [
|
|
49
52
|
language.onKey((event) => this.key(event), KEY_PRIORITY.pick),
|
|
50
53
|
editor.onTextChange(() => this.close()),
|
|
51
54
|
editor.onCaret(() => this.close()),
|
|
52
55
|
editor.onBlur(() => this.close())
|
|
53
56
|
];
|
|
57
|
+
|
|
54
58
|
language.onDispose(() => {
|
|
55
59
|
for (const off of offs) {
|
|
56
60
|
off();
|
|
57
61
|
}
|
|
62
|
+
|
|
58
63
|
this.close();
|
|
59
64
|
});
|
|
60
65
|
}
|
|
@@ -66,9 +71,11 @@ export class PickFeature {
|
|
|
66
71
|
open(spec: PickSpec): void {
|
|
67
72
|
this.close();
|
|
68
73
|
const ids = spec.groups.flatMap((group) => group.rows.map((row) => row.id));
|
|
74
|
+
|
|
69
75
|
if (ids.length === 0) {
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
78
|
+
|
|
72
79
|
this.spec = spec;
|
|
73
80
|
this.refusals = new Map();
|
|
74
81
|
this.ids = ids;
|
|
@@ -81,6 +88,7 @@ export class PickFeature {
|
|
|
81
88
|
if (this.spec === null) {
|
|
82
89
|
return;
|
|
83
90
|
}
|
|
91
|
+
|
|
84
92
|
this.spec = null;
|
|
85
93
|
this.timers.clear(this.previewTimer);
|
|
86
94
|
this.controller?.abort();
|
|
@@ -96,28 +104,37 @@ export class PickFeature {
|
|
|
96
104
|
/* Takes a row, by a click or Enter. */
|
|
97
105
|
choose(id: string): void {
|
|
98
106
|
const spec = this.spec;
|
|
107
|
+
|
|
99
108
|
if (spec === null || !this.ids.includes(id) || this.attempting) {
|
|
100
109
|
return;
|
|
101
110
|
}
|
|
111
|
+
|
|
102
112
|
if (spec.attempt === undefined) {
|
|
103
113
|
this.close();
|
|
104
114
|
spec.accept(id);
|
|
115
|
+
|
|
105
116
|
return;
|
|
106
117
|
}
|
|
118
|
+
|
|
107
119
|
this.attempting = true;
|
|
120
|
+
|
|
108
121
|
void spec
|
|
109
122
|
.attempt(id)
|
|
110
123
|
.catch(() => null)
|
|
111
124
|
.then((reason) => {
|
|
112
125
|
this.attempting = false;
|
|
126
|
+
|
|
113
127
|
if (this.spec !== spec) {
|
|
114
128
|
return;
|
|
115
129
|
}
|
|
130
|
+
|
|
116
131
|
if (reason === null) {
|
|
117
132
|
this.close();
|
|
118
133
|
spec.accept(id);
|
|
134
|
+
|
|
119
135
|
return;
|
|
120
136
|
}
|
|
137
|
+
|
|
121
138
|
this.refusals.set(id, reason);
|
|
122
139
|
this.publish(null);
|
|
123
140
|
});
|
|
@@ -127,6 +144,7 @@ export class PickFeature {
|
|
|
127
144
|
if (id === this.active) {
|
|
128
145
|
return;
|
|
129
146
|
}
|
|
147
|
+
|
|
130
148
|
this.active = id;
|
|
131
149
|
this.controller?.abort();
|
|
132
150
|
this.publish(null);
|
|
@@ -137,40 +155,51 @@ export class PickFeature {
|
|
|
137
155
|
if (this.spec === null || event.metaKey || event.ctrlKey || event.altKey) {
|
|
138
156
|
return false;
|
|
139
157
|
}
|
|
158
|
+
|
|
140
159
|
switch (event.key) {
|
|
141
160
|
case 'ArrowDown':
|
|
142
161
|
this.move(1);
|
|
162
|
+
|
|
143
163
|
return true;
|
|
144
164
|
case 'ArrowUp':
|
|
145
165
|
this.move(-1);
|
|
166
|
+
|
|
146
167
|
return true;
|
|
147
168
|
case 'PageDown':
|
|
148
169
|
this.move(8);
|
|
170
|
+
|
|
149
171
|
return true;
|
|
150
172
|
case 'PageUp':
|
|
151
173
|
this.move(-8);
|
|
174
|
+
|
|
152
175
|
return true;
|
|
153
176
|
case 'Home':
|
|
154
177
|
this.activate(this.ids[0]!);
|
|
178
|
+
|
|
155
179
|
return true;
|
|
156
180
|
case 'End':
|
|
157
181
|
this.activate(this.ids[this.ids.length - 1]!);
|
|
182
|
+
|
|
158
183
|
return true;
|
|
159
184
|
case 'Enter':
|
|
160
185
|
this.choose(this.active);
|
|
186
|
+
|
|
161
187
|
return true;
|
|
162
188
|
case 'Tab': {
|
|
163
189
|
const spec = this.spec;
|
|
190
|
+
|
|
164
191
|
if (spec.onTab !== undefined) {
|
|
165
192
|
this.close();
|
|
166
193
|
spec.onTab(event.shiftKey);
|
|
167
194
|
} else {
|
|
168
195
|
this.choose(this.active);
|
|
169
196
|
}
|
|
197
|
+
|
|
170
198
|
return true;
|
|
171
199
|
}
|
|
172
200
|
case 'Escape':
|
|
173
201
|
this.close();
|
|
202
|
+
|
|
174
203
|
return true;
|
|
175
204
|
default:
|
|
176
205
|
return false;
|
|
@@ -180,13 +209,17 @@ export class PickFeature {
|
|
|
180
209
|
private schedulePreview(): void {
|
|
181
210
|
const spec = this.spec;
|
|
182
211
|
this.timers.clear(this.previewTimer);
|
|
212
|
+
|
|
183
213
|
if (spec?.preview === undefined) {
|
|
184
214
|
return;
|
|
185
215
|
}
|
|
216
|
+
|
|
186
217
|
const id = this.active;
|
|
218
|
+
|
|
187
219
|
this.previewTimer = this.timers.set(() => {
|
|
188
220
|
const controller = new AbortController();
|
|
189
221
|
this.controller = controller;
|
|
222
|
+
|
|
190
223
|
spec.preview!(id, controller.signal)
|
|
191
224
|
.then((preview) => {
|
|
192
225
|
if (this.spec === spec && this.active === id && !controller.signal.aborted) {
|
|
@@ -199,9 +232,11 @@ export class PickFeature {
|
|
|
199
232
|
|
|
200
233
|
private publish(preview: PickPreview | null): void {
|
|
201
234
|
const spec = this.spec;
|
|
235
|
+
|
|
202
236
|
if (spec === null) {
|
|
203
237
|
return;
|
|
204
238
|
}
|
|
239
|
+
|
|
205
240
|
const refusal = this.refusals.get(this.active);
|
|
206
241
|
const shown = refusal === undefined ? preview : { removed: [], added: [], note: null, refusal };
|
|
207
242
|
this.language.popups.setState({ pick: { anchor: spec.anchor, groups: spec.groups, active: this.active, preview: shown, title: spec.title ?? null } });
|
package/src/popup-focus.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useLayoutEffect, type FocusEvent, type MouseEvent } from 'react';
|
|
2
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
* The handlers for a popup list whose rows the keyboard never enters: the text it belongs to keeps the
|
|
@@ -15,3 +16,12 @@ export function keepFocusOutOfList(focusBack: () => void): {
|
|
|
15
16
|
onFocus: () => focusBack()
|
|
16
17
|
};
|
|
17
18
|
}
|
|
19
|
+
|
|
20
|
+
/* The focus stays in the text, so the input is what names the popup's list and its active option. */
|
|
21
|
+
export function useInputPopup(language: EditorLanguage, controls: string, activeDescendant: string | undefined): void {
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
language.editor.setInputPopup({ controls, activeDescendant });
|
|
24
|
+
|
|
25
|
+
return () => language.editor.setInputPopup(null);
|
|
26
|
+
}, [language, controls, activeDescendant]);
|
|
27
|
+
}
|
package/src/popup-placement.ts
CHANGED
|
@@ -38,6 +38,7 @@ export function placePopup(anchor: EditorRect, size: PopupSize, window: PopupSiz
|
|
|
38
38
|
const maxHeight = Math.max(0, below ? roomBelow : roomAbove);
|
|
39
39
|
const height = Math.min(size.height, maxHeight);
|
|
40
40
|
const left = Math.max(margin, Math.min(anchor.left, window.width - size.width - margin));
|
|
41
|
+
|
|
41
42
|
return { left, top: below ? anchor.bottom + gap : anchor.top - gap - height, maxHeight, side: below ? 'below' : 'above' };
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -54,11 +55,15 @@ export function placeBeside(
|
|
|
54
55
|
): PopupPlacement {
|
|
55
56
|
const fitsRight = first.left + first.width + gap + size.width <= window.width - margin;
|
|
56
57
|
const fitsLeft = first.left - gap - size.width >= margin;
|
|
58
|
+
|
|
57
59
|
if (fitsRight || fitsLeft) {
|
|
58
60
|
const top = Math.max(margin, Math.min(first.top, window.height - size.height - margin));
|
|
61
|
+
|
|
59
62
|
return { left: fitsRight ? first.left + first.width + gap : first.left - gap - size.width, top, maxHeight: window.height - margin * 2, side: 'below' };
|
|
60
63
|
}
|
|
64
|
+
|
|
61
65
|
const top = first.top + first.height + gap;
|
|
62
66
|
const maxHeight = Math.max(0, window.height - margin - top);
|
|
67
|
+
|
|
63
68
|
return { left: Math.max(margin, Math.min(first.left, window.width - size.width - margin)), top, maxHeight, side: 'below' };
|
|
64
69
|
}
|
package/src/popups.ts
CHANGED
|
@@ -20,7 +20,6 @@ export interface HoverView {
|
|
|
20
20
|
/* While the pointer stays in this range the card stays, so moving along a word does not make it blink. */
|
|
21
21
|
readonly subject: EditorRange;
|
|
22
22
|
readonly problems: readonly Problem[];
|
|
23
|
-
/* What the servers say about the symbol, and where it is defined. */
|
|
24
23
|
readonly info: HoverInfo | null;
|
|
25
24
|
}
|
|
26
25
|
|
|
@@ -29,13 +28,12 @@ export interface HoverInfo {
|
|
|
29
28
|
readonly definition: Location | null;
|
|
30
29
|
/* The name under the pointer, which a signature's own symbol is told by. */
|
|
31
30
|
readonly word: string;
|
|
32
|
-
/* How many other places use the symbol
|
|
31
|
+
/* How many other places use the symbol; null until a server has counted them. */
|
|
33
32
|
readonly references: number | null;
|
|
34
|
-
/* Only the text
|
|
33
|
+
/* Only the text, without the footer for the definition and the references. */
|
|
35
34
|
readonly plain?: boolean;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
/* One line of the suggestions list. */
|
|
39
37
|
export interface CompletionRow {
|
|
40
38
|
readonly label: string;
|
|
41
39
|
readonly kind: number | undefined;
|
|
@@ -56,7 +54,6 @@ export interface CompletionDocs {
|
|
|
56
54
|
readonly text: HoverText;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
/* The suggestions while a word is being typed. */
|
|
60
57
|
export interface CompletionView {
|
|
61
58
|
/* The start of the word, which the list is placed under. */
|
|
62
59
|
readonly anchor: EditorPosition;
|
|
@@ -65,7 +62,6 @@ export interface CompletionView {
|
|
|
65
62
|
/* Whether the documentation of the active row is shown beside the list. */
|
|
66
63
|
readonly detailsOpen: boolean;
|
|
67
64
|
readonly docs: CompletionDocs | null;
|
|
68
|
-
/* The server the suggestions came from. */
|
|
69
65
|
readonly server: string;
|
|
70
66
|
/* The shiki id of the file's language, for the signature in the documentation. */
|
|
71
67
|
readonly highlightLanguage: string;
|
package/src/project-language.ts
CHANGED
|
@@ -32,12 +32,15 @@ class DocumentSync {
|
|
|
32
32
|
this.uri = uri;
|
|
33
33
|
this.editor = editor;
|
|
34
34
|
this.stop = editor.onTextChange((change) => this.changed(change.changes));
|
|
35
|
+
|
|
35
36
|
this.ready = service.openDocument({ uri, languageId, text: editor.getText() }).then(() => {
|
|
36
37
|
this.opened = true;
|
|
38
|
+
|
|
37
39
|
if (!this.disposed) {
|
|
38
40
|
this.flush();
|
|
39
41
|
}
|
|
40
42
|
});
|
|
43
|
+
|
|
41
44
|
// A failed open is also a document nothing can be asked of; the features' own requests say so.
|
|
42
45
|
this.ready.catch(() => undefined);
|
|
43
46
|
}
|
|
@@ -54,6 +57,7 @@ class DocumentSync {
|
|
|
54
57
|
|
|
55
58
|
private changed(changes: readonly ContentChange[]): void {
|
|
56
59
|
this.pending.push(...changes);
|
|
60
|
+
|
|
57
61
|
if (this.opened) {
|
|
58
62
|
this.flush();
|
|
59
63
|
}
|
|
@@ -63,6 +67,7 @@ class DocumentSync {
|
|
|
63
67
|
if (this.pending.length === 0) {
|
|
64
68
|
return;
|
|
65
69
|
}
|
|
70
|
+
|
|
66
71
|
const changes = this.pending;
|
|
67
72
|
this.pending = [];
|
|
68
73
|
void this.service.changeDocument(this.uri, changes).catch(() => undefined);
|
|
@@ -82,6 +87,7 @@ function filesOf(targets: readonly FileOperationTarget[]): Array<{ uri: string;
|
|
|
82
87
|
return targets.map((target) => {
|
|
83
88
|
const { path, directory } =
|
|
84
89
|
typeof target === 'string' ? { path: target, directory: false } : { path: target.path, directory: target.directory === true };
|
|
90
|
+
|
|
85
91
|
return { uri: pathToFileUri(path), directory };
|
|
86
92
|
});
|
|
87
93
|
}
|
|
@@ -99,8 +105,10 @@ export class ProjectLanguage {
|
|
|
99
105
|
readonly history = new NavigationHistory();
|
|
100
106
|
private readonly holders = new Map<string, Holder>();
|
|
101
107
|
private readonly files: ProjectFiles | null;
|
|
108
|
+
|
|
102
109
|
/* The body height of the peek and call hierarchy panels, shared by the editors of the project. */
|
|
103
110
|
panelBodyHeight: number;
|
|
111
|
+
|
|
104
112
|
private readonly panelListeners = new Set<(height: number) => void>();
|
|
105
113
|
private pendingCaret: Place | null = null;
|
|
106
114
|
private lastPlace: Place | null = null;
|
|
@@ -119,6 +127,7 @@ export class ProjectLanguage {
|
|
|
119
127
|
/* Open panels of other editors of the project follow a resize. */
|
|
120
128
|
onPanelBodyChange(listener: (height: number) => void): () => void {
|
|
121
129
|
this.panelListeners.add(listener);
|
|
130
|
+
|
|
122
131
|
return () => this.panelListeners.delete(listener);
|
|
123
132
|
}
|
|
124
133
|
|
|
@@ -126,6 +135,7 @@ export class ProjectLanguage {
|
|
|
126
135
|
if (height === this.panelBodyHeight) {
|
|
127
136
|
return;
|
|
128
137
|
}
|
|
138
|
+
|
|
129
139
|
this.panelBodyHeight = height;
|
|
130
140
|
this.panelListeners.forEach((listener) => listener(height));
|
|
131
141
|
this.host.onPanelResize?.(height);
|
|
@@ -135,13 +145,16 @@ export class ProjectLanguage {
|
|
|
135
145
|
if (this.host.pathOfUri !== undefined) {
|
|
136
146
|
return this.host.pathOfUri(uri);
|
|
137
147
|
}
|
|
148
|
+
|
|
138
149
|
const path = fileUriToPath(uri);
|
|
150
|
+
|
|
139
151
|
return path !== null && this.folder !== '' ? relativeTo(path, this.folder) : path;
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
/* Makes an edit of a language server: an open document takes it as one undo step and any other file gets an unsaved draft. */
|
|
143
155
|
applyWorkspaceEdit(edit: WorkspaceEdit): Promise<ApplyWorkspaceEditResult> {
|
|
144
156
|
const { service } = this;
|
|
157
|
+
|
|
145
158
|
return applyWorkspaceEdit(edit, {
|
|
146
159
|
editorOf: (uri) => this.holders.get(uri)?.editors[0],
|
|
147
160
|
files: this.files,
|
|
@@ -175,10 +188,13 @@ export class ProjectLanguage {
|
|
|
175
188
|
/* The text of a file as it stands now: an open document's, else a draft's or the machine's; null when it is no text file. */
|
|
176
189
|
async readText(uri: string): Promise<string | null> {
|
|
177
190
|
const open = this.holders.get(uri)?.sync.text;
|
|
191
|
+
|
|
178
192
|
if (open !== undefined) {
|
|
179
193
|
return open;
|
|
180
194
|
}
|
|
195
|
+
|
|
181
196
|
const path = fileUriToPath(uri);
|
|
197
|
+
|
|
182
198
|
return path === null || this.files === null ? null : ((await this.files.read(path))?.text ?? null);
|
|
183
199
|
}
|
|
184
200
|
|
|
@@ -201,16 +217,20 @@ export class ProjectLanguage {
|
|
|
201
217
|
if (this.lastPlace !== null) {
|
|
202
218
|
this.history.record(this.lastPlace);
|
|
203
219
|
}
|
|
220
|
+
|
|
204
221
|
this.openPlace(place);
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
/* The column an opened place waits for, once the editor of its file has put its caret on the line; each place is asked for once. */
|
|
208
225
|
takeCaret(uri: string, line: number): EditorPosition | null {
|
|
209
226
|
const pending = this.pendingCaret;
|
|
227
|
+
|
|
210
228
|
if (pending === null || pending.uri !== uri || pending.position.line !== line) {
|
|
211
229
|
return null;
|
|
212
230
|
}
|
|
231
|
+
|
|
213
232
|
this.pendingCaret = null;
|
|
233
|
+
|
|
214
234
|
return pending.position;
|
|
215
235
|
}
|
|
216
236
|
|
|
@@ -219,13 +239,17 @@ export class ProjectLanguage {
|
|
|
219
239
|
if (this.disposed) {
|
|
220
240
|
throw new Error('The project language is disposed');
|
|
221
241
|
}
|
|
242
|
+
|
|
222
243
|
let holder = this.holders.get(uri);
|
|
244
|
+
|
|
223
245
|
if (holder === undefined) {
|
|
224
246
|
holder = { editors: [], sync: new DocumentSync(this.service, uri, languageId, editor), languageId };
|
|
225
247
|
this.holders.set(uri, holder);
|
|
226
248
|
}
|
|
249
|
+
|
|
227
250
|
holder.editors.push(editor);
|
|
228
251
|
let released = false;
|
|
252
|
+
|
|
229
253
|
return {
|
|
230
254
|
uri,
|
|
231
255
|
service: this.service,
|
|
@@ -243,20 +267,25 @@ export class ProjectLanguage {
|
|
|
243
267
|
|
|
244
268
|
private release(uri: string, editor: Editor): void {
|
|
245
269
|
const holder = this.holders.get(uri);
|
|
270
|
+
|
|
246
271
|
if (holder === undefined) {
|
|
247
272
|
return;
|
|
248
273
|
}
|
|
274
|
+
|
|
249
275
|
holder.editors.splice(holder.editors.indexOf(editor), 1);
|
|
250
276
|
const next = holder.editors[0];
|
|
277
|
+
|
|
251
278
|
if (next === undefined) {
|
|
252
279
|
holder.sync.dispose();
|
|
253
280
|
this.holders.delete(uri);
|
|
281
|
+
|
|
254
282
|
void holder.sync.ready
|
|
255
283
|
.catch(() => undefined)
|
|
256
284
|
.then(() => {
|
|
257
285
|
if (this.holders.has(uri)) {
|
|
258
286
|
return undefined;
|
|
259
287
|
}
|
|
288
|
+
|
|
260
289
|
return this.service.closeDocument(uri);
|
|
261
290
|
})
|
|
262
291
|
.catch(() => undefined);
|
|
@@ -271,15 +300,19 @@ export class ProjectLanguage {
|
|
|
271
300
|
if (this.disposed) {
|
|
272
301
|
return;
|
|
273
302
|
}
|
|
303
|
+
|
|
274
304
|
this.disposed = true;
|
|
275
305
|
disposeSnippetTransforms(this);
|
|
306
|
+
|
|
276
307
|
for (const [uri, holder] of this.holders) {
|
|
277
308
|
holder.sync.dispose();
|
|
309
|
+
|
|
278
310
|
void holder.sync.ready
|
|
279
311
|
.catch(() => undefined)
|
|
280
312
|
.then(() => this.service.closeDocument(uri))
|
|
281
313
|
.catch(() => undefined);
|
|
282
314
|
}
|
|
315
|
+
|
|
283
316
|
this.holders.clear();
|
|
284
317
|
this.problems.dispose();
|
|
285
318
|
}
|