@adecore/editor-react 0.41.1 → 0.43.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.js +177 -181
- package/dist/EditorContextMenu.js +118 -105
- package/dist/EditorView.js +7 -1
- package/dist/LanguagePopups.js +7 -0
- package/dist/PickPopup.js +190 -70
- package/dist/WrapCard.d.ts +8 -0
- package/dist/WrapCard.js +172 -0
- package/dist/call-hierarchy-model.d.ts +4 -2
- package/dist/call-hierarchy-model.js +20 -0
- package/dist/call-hierarchy.d.ts +3 -0
- package/dist/call-hierarchy.js +54 -22
- package/dist/clipboard-history.d.ts +31 -0
- package/dist/clipboard-history.js +70 -0
- package/dist/code-actions.d.ts +3 -1
- package/dist/code-actions.js +50 -10
- package/dist/code-vision.js +2 -3
- package/dist/completion.js +10 -15
- package/dist/context-menu-rows.d.ts +1 -1
- package/dist/context-menu-rows.js +4 -0
- package/dist/definition-link.js +15 -5
- package/dist/document-colors.d.ts +24 -0
- package/dist/document-colors.js +194 -0
- package/dist/document-links.d.ts +22 -0
- package/dist/document-links.js +133 -0
- package/dist/document-query.d.ts +15 -0
- package/dist/document-query.js +54 -0
- package/dist/edit-locations.d.ts +20 -0
- package/dist/edit-locations.js +136 -0
- package/dist/editor-language.d.ts +19 -1
- package/dist/editor-language.js +77 -2
- package/dist/emmet.d.ts +13 -0
- package/dist/emmet.js +62 -0
- package/dist/folding.js +2 -3
- package/dist/format-on-type.js +3 -5
- package/dist/highlights.js +2 -3
- package/dist/history.d.ts +7 -0
- package/dist/history.js +79 -1
- package/dist/host-types.d.ts +6 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +8 -0
- package/dist/inlay-hints.js +2 -2
- package/dist/inline-completion.d.ts +39 -0
- package/dist/inline-completion.js +221 -0
- package/dist/key-priority.d.ts +8 -5
- package/dist/key-priority.js +9 -6
- package/dist/linked-editing.d.ts +26 -0
- package/dist/linked-editing.js +215 -0
- package/dist/locales/en.json +31 -2
- package/dist/locales/nl.json +31 -2
- package/dist/markup-context.d.ts +37 -0
- package/dist/markup-context.js +291 -0
- package/dist/markup.d.ts +36 -0
- package/dist/markup.js +453 -0
- package/dist/navigation.js +4 -6
- package/dist/paste-history.d.ts +7 -0
- package/dist/paste-history.js +104 -0
- package/dist/peek.js +3 -4
- package/dist/pick.d.ts +20 -2
- package/dist/pick.js +141 -18
- package/dist/popups.d.ts +9 -0
- package/dist/popups.js +2 -1
- package/dist/project-language.d.ts +18 -1
- package/dist/project-language.js +129 -18
- package/dist/rename.js +8 -11
- package/dist/rendered-docs.js +1 -2
- package/dist/selection-ranges.js +2 -3
- package/dist/shortcut-keys.d.ts +27 -0
- package/dist/signature.js +3 -6
- package/dist/snippet-session.d.ts +2 -2
- package/dist/snippet-session.js +3 -3
- package/dist/symbol-picker.js +4 -6
- package/dist/symbols.js +22 -3
- package/dist/testing.d.ts +23 -1
- package/dist/testing.js +114 -0
- package/package.json +7 -6
- package/src/CallHierarchyPanel.tsx +26 -11
- package/src/EditorContextMenu.tsx +7 -0
- package/src/EditorView.tsx +9 -0
- package/src/LanguagePopups.tsx +4 -0
- package/src/PickPopup.tsx +40 -5
- package/src/WrapCard.tsx +64 -0
- package/src/call-hierarchy-model.ts +25 -3
- package/src/call-hierarchy.ts +62 -20
- package/src/clipboard-history.ts +102 -0
- package/src/code-actions.ts +48 -11
- package/src/code-vision.ts +3 -3
- package/src/completion.ts +15 -15
- package/src/context-menu-rows.ts +3 -2
- package/src/definition-link.ts +17 -5
- package/src/document-colors.ts +189 -0
- package/src/document-links.ts +129 -0
- package/src/document-query.ts +71 -0
- package/src/edit-locations.ts +162 -0
- package/src/editor-language.ts +80 -3
- package/src/emmet-module.d.ts +44 -0
- package/src/emmet.ts +73 -0
- package/src/folding.ts +3 -3
- package/src/format-on-type.ts +5 -5
- package/src/highlights.ts +3 -3
- package/src/history.ts +81 -1
- package/src/host-types.ts +10 -0
- package/src/index.ts +12 -3
- package/src/inlay-hints.ts +2 -2
- package/src/inline-completion.ts +221 -0
- package/src/key-priority.ts +9 -6
- package/src/linked-editing.ts +199 -0
- package/src/locales/en.json +31 -2
- package/src/locales/nl.json +31 -2
- package/src/markup-context.ts +262 -0
- package/src/markup.ts +367 -0
- package/src/navigation.ts +6 -6
- package/src/paste-history.ts +111 -0
- package/src/peek.ts +4 -4
- package/src/pick.ts +201 -19
- package/src/popups.ts +19 -1
- package/src/project-language.ts +136 -19
- package/src/rename.ts +11 -11
- package/src/rendered-docs.ts +2 -2
- package/src/selection-ranges.ts +3 -3
- package/src/signature.ts +6 -6
- package/src/snippet-session.ts +9 -4
- package/src/symbol-picker.ts +6 -6
- package/src/symbols.ts +30 -4
- package/src/testing.ts +126 -1
package/src/pick.ts
CHANGED
|
@@ -22,6 +22,17 @@ export interface PickSpec {
|
|
|
22
22
|
attempt?(id: string): Promise<string | null>;
|
|
23
23
|
/* What the active row would change, asked a moment after it became active. */
|
|
24
24
|
preview?(id: string, signal: AbortSignal): Promise<PickPreview | null>;
|
|
25
|
+
/*
|
|
26
|
+
* Typing narrows the rows to those whose `search` (the label without one) contains the query, whatever the case, and
|
|
27
|
+
* the list stays open while a person types. Backspace edits the query.
|
|
28
|
+
*/
|
|
29
|
+
readonly filter?: boolean;
|
|
30
|
+
/* Shift with the arrows or a press chooses a run of rows, and Mod with a press adds or takes away one. */
|
|
31
|
+
readonly multiple?: boolean;
|
|
32
|
+
/* Enter takes the chosen rows, or the active one when none is chosen; without it Enter calls `accept` with the active row. */
|
|
33
|
+
acceptMany?(ids: readonly string[]): void;
|
|
34
|
+
/* Delete takes the chosen rows, or the active one, out of the list, and so does Backspace when there is no query. It answers the rows that remain. */
|
|
35
|
+
remove?(ids: readonly string[]): readonly PickGroup[];
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
/*
|
|
@@ -33,8 +44,18 @@ export class PickFeature {
|
|
|
33
44
|
private readonly language: EditorLanguage;
|
|
34
45
|
private readonly timers: Timers;
|
|
35
46
|
private spec: PickSpec | null = null;
|
|
47
|
+
|
|
48
|
+
/* The rows as the spec gave them, or as `remove` left them; `ids` are those a query lets through. */
|
|
49
|
+
private groups: readonly PickGroup[] = [];
|
|
50
|
+
|
|
36
51
|
private ids: readonly string[] = [];
|
|
37
52
|
private active = '';
|
|
53
|
+
private query = '';
|
|
54
|
+
|
|
55
|
+
/* The rows chosen as a run or one by one, in list order, and the row a run of them grows from. */
|
|
56
|
+
private chosen: readonly string[] = [];
|
|
57
|
+
|
|
58
|
+
private rangeAnchor: string | null = null;
|
|
38
59
|
private previewTimer: unknown;
|
|
39
60
|
private controller: AbortController | null = null;
|
|
40
61
|
|
|
@@ -78,6 +99,10 @@ export class PickFeature {
|
|
|
78
99
|
|
|
79
100
|
this.spec = spec;
|
|
80
101
|
this.refusals = new Map();
|
|
102
|
+
this.groups = spec.groups;
|
|
103
|
+
this.query = '';
|
|
104
|
+
this.chosen = [];
|
|
105
|
+
this.rangeAnchor = null;
|
|
81
106
|
this.ids = ids;
|
|
82
107
|
this.active = spec.active !== undefined && ids.includes(spec.active) ? spec.active : ids[0]!;
|
|
83
108
|
this.publish(null);
|
|
@@ -96,19 +121,53 @@ export class PickFeature {
|
|
|
96
121
|
this.language.popups.setState({ pick: null });
|
|
97
122
|
}
|
|
98
123
|
|
|
99
|
-
move
|
|
124
|
+
/* Moves the active row; with `extend`, in a list of several choices, the rows between where the move began and the new row are chosen. */
|
|
125
|
+
move(delta: number, extend = false): void {
|
|
126
|
+
if (this.ids.length === 0) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
100
130
|
const index = this.ids.indexOf(this.active);
|
|
101
|
-
this.
|
|
131
|
+
const id = this.ids[Math.max(0, Math.min(this.ids.length - 1, index + delta))]!;
|
|
132
|
+
|
|
133
|
+
if (extend && this.spec?.multiple === true) {
|
|
134
|
+
this.extendTo(id);
|
|
135
|
+
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.settle(id);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Makes `id` the only row the list is on. */
|
|
143
|
+
private settle(id: string): void {
|
|
144
|
+
const hadChosen = this.chosen.length > 0;
|
|
145
|
+
|
|
146
|
+
this.chosen = [];
|
|
147
|
+
this.rangeAnchor = null;
|
|
148
|
+
this.activate(id, hadChosen);
|
|
102
149
|
}
|
|
103
150
|
|
|
104
|
-
/* Takes a row, by a click or Enter. */
|
|
105
|
-
choose(id: string): void {
|
|
151
|
+
/* Takes a row, by a click or Enter. In a list of several choices a press with Shift chooses the run up to the row, and one with Mod adds or takes away the row. */
|
|
152
|
+
choose(id: string, modifiers: { readonly toggle?: boolean; readonly range?: boolean } = {}): void {
|
|
106
153
|
const spec = this.spec;
|
|
107
154
|
|
|
108
155
|
if (spec === null || !this.ids.includes(id) || this.attempting) {
|
|
109
156
|
return;
|
|
110
157
|
}
|
|
111
158
|
|
|
159
|
+
if (spec.multiple === true && modifiers.range === true) {
|
|
160
|
+
this.extendTo(id);
|
|
161
|
+
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (spec.multiple === true && modifiers.toggle === true) {
|
|
166
|
+
this.toggle(id);
|
|
167
|
+
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
112
171
|
if (spec.attempt === undefined) {
|
|
113
172
|
this.close();
|
|
114
173
|
spec.accept(id);
|
|
@@ -140,29 +199,124 @@ export class PickFeature {
|
|
|
140
199
|
});
|
|
141
200
|
}
|
|
142
201
|
|
|
143
|
-
private activate(id: string): void {
|
|
144
|
-
|
|
202
|
+
private activate(id: string, force = false): void {
|
|
203
|
+
const moved = id !== this.active;
|
|
204
|
+
|
|
205
|
+
this.active = id;
|
|
206
|
+
|
|
207
|
+
if (moved) {
|
|
208
|
+
this.controller?.abort();
|
|
209
|
+
this.schedulePreview();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (moved || force) {
|
|
213
|
+
this.publish(null);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private extendTo(id: string): void {
|
|
218
|
+
const anchor = this.rangeAnchor ?? this.active;
|
|
219
|
+
const first = this.ids.indexOf(anchor);
|
|
220
|
+
const last = this.ids.indexOf(id);
|
|
221
|
+
const run = this.ids.slice(Math.min(first, last), Math.max(first, last) + 1);
|
|
222
|
+
|
|
223
|
+
this.rangeAnchor = anchor;
|
|
224
|
+
this.chosen = run.length > 1 ? run : [];
|
|
225
|
+
this.activate(id, true);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private toggle(id: string): void {
|
|
229
|
+
const held = this.chosen.length > 0 ? this.chosen : [this.active];
|
|
230
|
+
const next = held.includes(id) ? held.filter((other) => other !== id) : [...held, id];
|
|
231
|
+
|
|
232
|
+
this.chosen = this.ids.filter((other) => next.includes(other));
|
|
233
|
+
this.rangeAnchor = id;
|
|
234
|
+
this.activate(id, true);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Enter: the chosen rows, or the active one when none is chosen. */
|
|
238
|
+
private takeActive(): void {
|
|
239
|
+
const spec = this.spec;
|
|
240
|
+
|
|
241
|
+
if (spec?.acceptMany === undefined) {
|
|
242
|
+
this.choose(this.active);
|
|
243
|
+
|
|
145
244
|
return;
|
|
146
245
|
}
|
|
147
246
|
|
|
148
|
-
this.
|
|
247
|
+
const ids = this.chosen.length > 0 ? this.chosen : [this.active];
|
|
248
|
+
|
|
249
|
+
if (this.active !== '' && !this.attempting) {
|
|
250
|
+
this.close();
|
|
251
|
+
spec.acceptMany(ids);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private visibleGroups(): readonly PickGroup[] {
|
|
256
|
+
const query = this.query.toLowerCase();
|
|
257
|
+
|
|
258
|
+
if (query === '') {
|
|
259
|
+
return this.groups;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return this.groups
|
|
263
|
+
.map((group) => ({ ...group, rows: group.rows.filter((row) => (row.search ?? row.label).toLowerCase().includes(query)) }))
|
|
264
|
+
.filter((group) => group.rows.length > 0);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* The query changed: the rows it lets through, with the first of them active. */
|
|
268
|
+
private requery(query: string): void {
|
|
269
|
+
this.query = query;
|
|
270
|
+
this.ids = this.visibleGroups().flatMap((group) => group.rows.map((row) => row.id));
|
|
271
|
+
this.chosen = [];
|
|
272
|
+
this.rangeAnchor = null;
|
|
273
|
+
this.active = this.ids[0] ?? '';
|
|
274
|
+
this.controller?.abort();
|
|
275
|
+
this.publish(null);
|
|
276
|
+
this.schedulePreview();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* Takes the chosen rows, or the active one, out of the list; the list closes when no row is left. */
|
|
280
|
+
private removeChosen(): void {
|
|
281
|
+
const spec = this.spec;
|
|
282
|
+
|
|
283
|
+
if (spec?.remove === undefined || this.active === '') {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const removed = this.chosen.length > 0 ? this.chosen : [this.active];
|
|
288
|
+
const index = this.ids.indexOf(removed[0]!);
|
|
289
|
+
this.groups = spec.remove(removed);
|
|
290
|
+
|
|
291
|
+
if (this.groups.every((group) => group.rows.length === 0)) {
|
|
292
|
+
this.close();
|
|
293
|
+
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.ids = this.visibleGroups().flatMap((group) => group.rows.map((row) => row.id));
|
|
298
|
+
this.chosen = [];
|
|
299
|
+
this.rangeAnchor = null;
|
|
300
|
+
this.active = this.ids[Math.min(index, this.ids.length - 1)] ?? '';
|
|
149
301
|
this.controller?.abort();
|
|
150
302
|
this.publish(null);
|
|
151
303
|
this.schedulePreview();
|
|
152
304
|
}
|
|
153
305
|
|
|
154
306
|
private key(event: KeyboardEvent): boolean {
|
|
155
|
-
|
|
307
|
+
const spec = this.spec;
|
|
308
|
+
|
|
309
|
+
if (spec === null || event.metaKey || event.ctrlKey || event.altKey) {
|
|
156
310
|
return false;
|
|
157
311
|
}
|
|
158
312
|
|
|
159
313
|
switch (event.key) {
|
|
160
314
|
case 'ArrowDown':
|
|
161
|
-
this.move(1);
|
|
315
|
+
this.move(1, event.shiftKey);
|
|
162
316
|
|
|
163
317
|
return true;
|
|
164
318
|
case 'ArrowUp':
|
|
165
|
-
this.move(-1);
|
|
319
|
+
this.move(-1, event.shiftKey);
|
|
166
320
|
|
|
167
321
|
return true;
|
|
168
322
|
case 'PageDown':
|
|
@@ -174,20 +328,31 @@ export class PickFeature {
|
|
|
174
328
|
|
|
175
329
|
return true;
|
|
176
330
|
case 'Home':
|
|
177
|
-
this.activate(this.ids[0]!);
|
|
178
|
-
|
|
179
|
-
return true;
|
|
180
331
|
case 'End':
|
|
181
|
-
|
|
332
|
+
if (this.ids.length > 0) {
|
|
333
|
+
this.settle(event.key === 'Home' ? this.ids[0]! : this.ids[this.ids.length - 1]!);
|
|
334
|
+
}
|
|
182
335
|
|
|
183
336
|
return true;
|
|
184
337
|
case 'Enter':
|
|
185
|
-
this.
|
|
338
|
+
this.takeActive();
|
|
186
339
|
|
|
187
340
|
return true;
|
|
188
|
-
case '
|
|
189
|
-
|
|
341
|
+
case 'Delete':
|
|
342
|
+
this.removeChosen();
|
|
343
|
+
|
|
344
|
+
return spec.remove !== undefined;
|
|
345
|
+
case 'Backspace':
|
|
346
|
+
if (this.query !== '') {
|
|
347
|
+
this.requery(this.query.slice(0, -1));
|
|
190
348
|
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.removeChosen();
|
|
353
|
+
|
|
354
|
+
return spec.remove !== undefined || spec.filter === true;
|
|
355
|
+
case 'Tab': {
|
|
191
356
|
if (spec.onTab !== undefined) {
|
|
192
357
|
this.close();
|
|
193
358
|
spec.onTab(event.shiftKey);
|
|
@@ -202,6 +367,12 @@ export class PickFeature {
|
|
|
202
367
|
|
|
203
368
|
return true;
|
|
204
369
|
default:
|
|
370
|
+
if (spec.filter === true && event.key.length === 1) {
|
|
371
|
+
this.requery(this.query + event.key);
|
|
372
|
+
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
205
376
|
return false;
|
|
206
377
|
}
|
|
207
378
|
}
|
|
@@ -210,7 +381,7 @@ export class PickFeature {
|
|
|
210
381
|
const spec = this.spec;
|
|
211
382
|
this.timers.clear(this.previewTimer);
|
|
212
383
|
|
|
213
|
-
if (spec?.preview === undefined) {
|
|
384
|
+
if (spec?.preview === undefined || this.active === '') {
|
|
214
385
|
return;
|
|
215
386
|
}
|
|
216
387
|
|
|
@@ -239,6 +410,17 @@ export class PickFeature {
|
|
|
239
410
|
|
|
240
411
|
const refusal = this.refusals.get(this.active);
|
|
241
412
|
const shown = refusal === undefined ? preview : { removed: [], added: [], note: null, refusal };
|
|
242
|
-
|
|
413
|
+
|
|
414
|
+
this.language.popups.setState({
|
|
415
|
+
pick: {
|
|
416
|
+
anchor: spec.anchor,
|
|
417
|
+
groups: this.visibleGroups(),
|
|
418
|
+
active: this.active,
|
|
419
|
+
preview: shown,
|
|
420
|
+
title: spec.title ?? null,
|
|
421
|
+
query: spec.filter === true ? this.query : null,
|
|
422
|
+
chosen: this.chosen
|
|
423
|
+
}
|
|
424
|
+
});
|
|
243
425
|
}
|
|
244
426
|
}
|
package/src/popups.ts
CHANGED
|
@@ -82,6 +82,8 @@ export interface PickRow {
|
|
|
82
82
|
readonly detail: string;
|
|
83
83
|
/* The detail is a path, which gives way from its start before the label gives way at all. */
|
|
84
84
|
readonly path?: boolean;
|
|
85
|
+
/* What a typed query is matched against, when a list filters; the label without it. */
|
|
86
|
+
readonly search?: string;
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
export interface PickGroup {
|
|
@@ -97,15 +99,22 @@ export interface PickPreview {
|
|
|
97
99
|
readonly note: string | null;
|
|
98
100
|
/* Why the action cannot be taken here, which the server said; nothing would change. */
|
|
99
101
|
readonly refusal?: string;
|
|
102
|
+
/* `diff` draws `removed` and `added` as changed lines, which is the default; `text` draws `added` as plain code, such as the content of a clipboard entry. */
|
|
103
|
+
readonly kind?: 'diff' | 'text';
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
/* A list under a character of the editor; Enter takes the active row, Escape closes it. */
|
|
103
107
|
export interface PickView {
|
|
104
108
|
readonly anchor: EditorPosition;
|
|
109
|
+
/* The rows a query lets through. */
|
|
105
110
|
readonly groups: readonly PickGroup[];
|
|
106
111
|
readonly active: string;
|
|
107
112
|
readonly preview: PickPreview | null;
|
|
108
113
|
readonly title: string | null;
|
|
114
|
+
/* What has been typed to narrow the rows; null for a list that does not filter. */
|
|
115
|
+
readonly query: string | null;
|
|
116
|
+
/* The rows chosen as a run or one by one, in list order; empty when none is, and then the active row is the choice. */
|
|
117
|
+
readonly chosen: readonly string[];
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
/* One file of a rename in the preview: the lines it changes. */
|
|
@@ -192,6 +201,13 @@ export interface AuthorsView {
|
|
|
192
201
|
readonly openCommit: (() => void) | null;
|
|
193
202
|
}
|
|
194
203
|
|
|
204
|
+
/* The field that asks for the abbreviation a selection is wrapped in. */
|
|
205
|
+
export interface WrapView {
|
|
206
|
+
readonly anchor: EditorPosition;
|
|
207
|
+
/* Why the last abbreviation did not expand; the field stays open for another. */
|
|
208
|
+
readonly error: string | null;
|
|
209
|
+
}
|
|
210
|
+
|
|
195
211
|
export interface PopupState {
|
|
196
212
|
readonly hover: HoverView | null;
|
|
197
213
|
readonly completion: CompletionView | null;
|
|
@@ -203,6 +219,7 @@ export interface PopupState {
|
|
|
203
219
|
readonly symbols: SymbolsView | null;
|
|
204
220
|
readonly menu: MenuView | null;
|
|
205
221
|
readonly authors: AuthorsView | null;
|
|
222
|
+
readonly wrap: WrapView | null;
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
export type PopupStore = StoreApi<PopupState>;
|
|
@@ -218,6 +235,7 @@ export function createPopupStore(): PopupStore {
|
|
|
218
235
|
callHierarchy: null,
|
|
219
236
|
symbols: null,
|
|
220
237
|
menu: null,
|
|
221
|
-
authors: null
|
|
238
|
+
authors: null,
|
|
239
|
+
wrap: null
|
|
222
240
|
}));
|
|
223
241
|
}
|
package/src/project-language.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { disposeSnippetTransforms } from './snippet-transform-queue.ts';
|
|
2
2
|
import i18next, { type i18n } from 'i18next';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
fileUriToPath,
|
|
5
|
+
pathToFileUri,
|
|
6
|
+
unservedLanguageService,
|
|
7
|
+
type ApplyWorkspaceEditResult,
|
|
8
|
+
type ContentChange,
|
|
9
|
+
type Disposable,
|
|
10
|
+
type LanguageService,
|
|
11
|
+
type ShowDocumentParams,
|
|
12
|
+
type ShowDocumentResult,
|
|
13
|
+
type WorkspaceEdit
|
|
14
|
+
} from '@adecore/lsp';
|
|
4
15
|
import type { Editor, EditorPosition } from '@adecore/editor';
|
|
16
|
+
import { ClipboardHistory } from './clipboard-history.ts';
|
|
17
|
+
import { EditLocations } from './edit-locations.ts';
|
|
5
18
|
import { NavigationHistory, type Place } from './navigation-history.ts';
|
|
6
19
|
import { ProjectProblems, serviceProblems } from './project-problems.ts';
|
|
7
20
|
import { applyWorkspaceEdit, type ProjectFiles } from './workspace-edit.ts';
|
|
@@ -16,6 +29,8 @@ export interface LanguageDocumentHandle {
|
|
|
16
29
|
release(): void;
|
|
17
30
|
}
|
|
18
31
|
|
|
32
|
+
const UNSERVED = unservedLanguageService();
|
|
33
|
+
|
|
19
34
|
/* Sends one editor's edits to the document, holding them until the document is open. */
|
|
20
35
|
class DocumentSync {
|
|
21
36
|
readonly ready: Promise<void>;
|
|
@@ -76,10 +91,18 @@ class DocumentSync {
|
|
|
76
91
|
|
|
77
92
|
interface Holder {
|
|
78
93
|
readonly editors: Editor[];
|
|
79
|
-
|
|
94
|
+
/* The editors a server serves, in the order they came; the first one sends the changes. */
|
|
95
|
+
readonly served: Editor[];
|
|
96
|
+
/* Null while no editor of the document is served, so nothing of it is open in the service. */
|
|
97
|
+
sync: DocumentSync | null;
|
|
80
98
|
languageId: string;
|
|
81
99
|
}
|
|
82
100
|
|
|
101
|
+
export interface AcquireOptions {
|
|
102
|
+
/* False registers the editor as open without telling the service, for a document no server serves; true by default. */
|
|
103
|
+
readonly serve?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
/* A file or folder for a file operation notification: a bare path is a file. */
|
|
84
107
|
export type FileOperationTarget = string | { readonly path: string; readonly directory?: boolean };
|
|
85
108
|
|
|
@@ -103,6 +126,8 @@ export class ProjectLanguage {
|
|
|
103
126
|
readonly folder: string;
|
|
104
127
|
readonly problems: ProjectProblems;
|
|
105
128
|
readonly history = new NavigationHistory();
|
|
129
|
+
readonly edits: EditLocations;
|
|
130
|
+
readonly clipboard: ClipboardHistory;
|
|
106
131
|
private readonly holders = new Map<string, Holder>();
|
|
107
132
|
private readonly files: ProjectFiles | null;
|
|
108
133
|
|
|
@@ -113,6 +138,9 @@ export class ProjectLanguage {
|
|
|
113
138
|
private pendingCaret: Place | null = null;
|
|
114
139
|
private lastPlace: Place | null = null;
|
|
115
140
|
private disposed = false;
|
|
141
|
+
private readonly subscriptions: readonly Disposable[];
|
|
142
|
+
/* The result id the server gave each document in its last workspace report, which the next asks with. */
|
|
143
|
+
private readonly workspaceResults = new Map<string, string>();
|
|
116
144
|
|
|
117
145
|
constructor(service: LanguageService, host: LanguageHost = {}) {
|
|
118
146
|
this.service = service;
|
|
@@ -120,8 +148,13 @@ export class ProjectLanguage {
|
|
|
120
148
|
this.i18n = host.i18n ?? i18next;
|
|
121
149
|
this.folder = host.folder ?? '';
|
|
122
150
|
this.files = host.files ?? null;
|
|
151
|
+
this.edits = new EditLocations(host.editHistoryLimit);
|
|
152
|
+
this.clipboard = host.clipboard ?? new ClipboardHistory();
|
|
123
153
|
this.panelBodyHeight = host.panelBodyHeight ?? PANEL_BODY_HEIGHT;
|
|
124
154
|
this.problems = new ProjectProblems(host.problemSource ?? serviceProblems(service, (uri) => this.pathOfLocation(uri) ?? uri));
|
|
155
|
+
const shows = service.onShowDocument?.((params) => this.showDocument(params));
|
|
156
|
+
const messages = host.onServerMessage === undefined ? undefined : service.onServerMessage?.((message) => host.onServerMessage!(message));
|
|
157
|
+
this.subscriptions = [shows, messages].flatMap((subscription) => (subscription === undefined ? [] : [subscription]));
|
|
125
158
|
}
|
|
126
159
|
|
|
127
160
|
/* Open panels of other editors of the project follow a resize. */
|
|
@@ -182,20 +215,73 @@ export class ProjectLanguage {
|
|
|
182
215
|
|
|
183
216
|
/* Tells the servers that files were deleted, once they are. */
|
|
184
217
|
didDeleteFiles(targets: readonly FileOperationTarget[]): Promise<void> {
|
|
185
|
-
|
|
218
|
+
const files = filesOf(targets);
|
|
219
|
+
|
|
220
|
+
this.edits.forget((uri) => files.some((file) => (file.directory ? uri.startsWith(`${file.uri}/`) : uri === file.uri)));
|
|
221
|
+
|
|
222
|
+
return this.service.didDeleteFiles?.(files) ?? Promise.resolve();
|
|
186
223
|
}
|
|
187
224
|
|
|
188
|
-
/*
|
|
225
|
+
/*
|
|
226
|
+
* 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
|
|
227
|
+
* text file. A document of a scheme a server owns, such as a decompiled class, is read from that server.
|
|
228
|
+
*/
|
|
189
229
|
async readText(uri: string): Promise<string | null> {
|
|
190
|
-
const open = this.holders.get(uri)?.
|
|
230
|
+
const open = this.holders.get(uri)?.editors[0]?.getText();
|
|
191
231
|
|
|
192
232
|
if (open !== undefined) {
|
|
193
233
|
return open;
|
|
194
234
|
}
|
|
195
235
|
|
|
196
236
|
const path = fileUriToPath(uri);
|
|
237
|
+
if (path === null) {
|
|
238
|
+
return (await this.service.textDocumentContent?.(uri).catch(() => null)) ?? null;
|
|
239
|
+
}
|
|
197
240
|
|
|
198
|
-
return
|
|
241
|
+
return this.files === null ? null : ((await this.files.read(path))?.text ?? null);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/*
|
|
245
|
+
* Asks the server of `uri` for the problems of every file it knows and lists them in `problems`, for a server that
|
|
246
|
+
* offers workspace diagnostics. A file the server reports as unchanged keeps what it had. False when no server offers them.
|
|
247
|
+
*/
|
|
248
|
+
async pullWorkspaceProblems(uri: string, options: { readonly signal?: AbortSignal } = {}): Promise<boolean> {
|
|
249
|
+
const { service } = this;
|
|
250
|
+
if (service.workspaceDiagnostics === undefined || !service.supports('workspace/diagnostic', uri)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const previous = [...this.workspaceResults].map(([key, value]) => ({ uri: key, value }));
|
|
254
|
+
const report = await service.workspaceDiagnostics(uri, previous, options);
|
|
255
|
+
const server = this.host.serverNames?.(uri)[0] ?? 'workspace';
|
|
256
|
+
for (const item of report.items) {
|
|
257
|
+
if (item.resultId !== undefined) {
|
|
258
|
+
this.workspaceResults.set(item.uri, item.resultId);
|
|
259
|
+
}
|
|
260
|
+
if (item.kind === 'full') {
|
|
261
|
+
this.problems.report(this.pathOfLocation(item.uri) ?? item.uri, server, item.items);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Shows what a server asks for: a web address outside the editor, anything else as a place of the host. */
|
|
268
|
+
private async showDocument(params: ShowDocumentParams): Promise<ShowDocumentResult> {
|
|
269
|
+
const web = /^(https?|mailto):/i.test(params.uri);
|
|
270
|
+
if (params.external === true || web) {
|
|
271
|
+
if (this.host.openExternal !== undefined) {
|
|
272
|
+
this.host.openExternal(params.uri);
|
|
273
|
+
return { success: true };
|
|
274
|
+
}
|
|
275
|
+
if (typeof window === 'undefined') {
|
|
276
|
+
return { success: false };
|
|
277
|
+
}
|
|
278
|
+
return { success: window.open(params.uri, '_blank', 'noopener,noreferrer') !== null };
|
|
279
|
+
}
|
|
280
|
+
if (this.host.openPlace === undefined) {
|
|
281
|
+
return { success: false };
|
|
282
|
+
}
|
|
283
|
+
this.openPlace({ uri: params.uri, position: params.selection?.start ?? { line: 0, character: 0 } });
|
|
284
|
+
return { success: true };
|
|
199
285
|
}
|
|
200
286
|
|
|
201
287
|
/*
|
|
@@ -234,27 +320,39 @@ export class ProjectLanguage {
|
|
|
234
320
|
return pending.position;
|
|
235
321
|
}
|
|
236
322
|
|
|
323
|
+
/* The editors that hold a document, one per uri in the order the documents were first opened. */
|
|
324
|
+
openEditors(): readonly { uri: string; editor: Editor }[] {
|
|
325
|
+
return [...this.holders].flatMap(([uri, holder]) => (holder.editors[0] === undefined ? [] : [{ uri, editor: holder.editors[0] }]));
|
|
326
|
+
}
|
|
327
|
+
|
|
237
328
|
/* Opens `uri` for the editor, or joins the editors that already hold it. */
|
|
238
|
-
acquire(uri: string, languageId: string, editor: Editor): LanguageDocumentHandle {
|
|
329
|
+
acquire(uri: string, languageId: string, editor: Editor, options: AcquireOptions = {}): LanguageDocumentHandle {
|
|
239
330
|
if (this.disposed) {
|
|
240
331
|
throw new Error('The project language is disposed');
|
|
241
332
|
}
|
|
242
333
|
|
|
334
|
+
const serve = options.serve ?? true;
|
|
243
335
|
let holder = this.holders.get(uri);
|
|
244
336
|
|
|
245
337
|
if (holder === undefined) {
|
|
246
|
-
holder = { editors: [],
|
|
338
|
+
holder = { editors: [], served: [], sync: null, languageId };
|
|
247
339
|
this.holders.set(uri, holder);
|
|
248
340
|
}
|
|
249
341
|
|
|
250
342
|
holder.editors.push(editor);
|
|
343
|
+
|
|
344
|
+
if (serve) {
|
|
345
|
+
holder.served.push(editor);
|
|
346
|
+
holder.sync ??= new DocumentSync(this.service, uri, languageId, editor);
|
|
347
|
+
}
|
|
348
|
+
|
|
251
349
|
let released = false;
|
|
252
350
|
|
|
253
351
|
return {
|
|
254
352
|
uri,
|
|
255
|
-
service: this.service,
|
|
353
|
+
service: serve ? this.service : UNSERVED,
|
|
256
354
|
get ready() {
|
|
257
|
-
return holder.sync.
|
|
355
|
+
return serve ? (holder.sync?.ready ?? Promise.resolve()) : Promise.resolve();
|
|
258
356
|
},
|
|
259
357
|
release: () => {
|
|
260
358
|
if (!released) {
|
|
@@ -273,26 +371,38 @@ export class ProjectLanguage {
|
|
|
273
371
|
}
|
|
274
372
|
|
|
275
373
|
holder.editors.splice(holder.editors.indexOf(editor), 1);
|
|
276
|
-
const next = holder.editors[0];
|
|
277
374
|
|
|
278
|
-
if (
|
|
279
|
-
holder.
|
|
375
|
+
if (holder.served.includes(editor)) {
|
|
376
|
+
holder.served.splice(holder.served.indexOf(editor), 1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const { sync } = holder;
|
|
380
|
+
|
|
381
|
+
if (holder.editors.length === 0) {
|
|
280
382
|
this.holders.delete(uri);
|
|
383
|
+
}
|
|
281
384
|
|
|
282
|
-
|
|
385
|
+
if (sync === null || sync.editor !== editor) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
sync.dispose();
|
|
390
|
+
const next = holder.served[0];
|
|
391
|
+
|
|
392
|
+
// The editor that sent the changes is gone; the next served one takes over and opens the document with its own text.
|
|
393
|
+
holder.sync = next === undefined ? null : new DocumentSync(this.service, uri, holder.languageId, next);
|
|
394
|
+
|
|
395
|
+
if (holder.sync === null) {
|
|
396
|
+
void sync.ready
|
|
283
397
|
.catch(() => undefined)
|
|
284
398
|
.then(() => {
|
|
285
|
-
if (this.holders.
|
|
399
|
+
if (this.holders.get(uri)?.sync) {
|
|
286
400
|
return undefined;
|
|
287
401
|
}
|
|
288
402
|
|
|
289
403
|
return this.service.closeDocument(uri);
|
|
290
404
|
})
|
|
291
405
|
.catch(() => undefined);
|
|
292
|
-
} else if (holder.sync.editor === editor) {
|
|
293
|
-
// The editor that sent the changes is gone; the next one takes over and opens the document with its own text.
|
|
294
|
-
holder.sync.dispose();
|
|
295
|
-
holder.sync = new DocumentSync(this.service, uri, holder.languageId, next);
|
|
296
406
|
}
|
|
297
407
|
}
|
|
298
408
|
|
|
@@ -302,9 +412,16 @@ export class ProjectLanguage {
|
|
|
302
412
|
}
|
|
303
413
|
|
|
304
414
|
this.disposed = true;
|
|
415
|
+
for (const subscription of this.subscriptions) {
|
|
416
|
+
subscription.dispose();
|
|
417
|
+
}
|
|
305
418
|
disposeSnippetTransforms(this);
|
|
306
419
|
|
|
307
420
|
for (const [uri, holder] of this.holders) {
|
|
421
|
+
if (holder.sync === null) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
308
425
|
holder.sync.dispose();
|
|
309
426
|
|
|
310
427
|
void holder.sync.ready
|