@adecore/editor-react 0.42.0 → 0.44.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/LanguagePopups.js +7 -0
- package/dist/PickPopup.js +245 -104
- 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.d.ts +28 -0
- package/dist/completion.js +83 -27
- 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 +23 -1
- package/dist/editor-language.js +83 -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 +10 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +10 -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 +9 -5
- package/dist/key-priority.js +11 -6
- package/dist/linked-editing.d.ts +26 -0
- package/dist/linked-editing.js +215 -0
- package/dist/live-templates.d.ts +16 -0
- package/dist/live-templates.js +289 -0
- package/dist/locales/en.json +61 -2
- package/dist/locales/nl.json +61 -2
- package/dist/markup-context.d.ts +55 -0
- package/dist/markup-context.js +408 -0
- package/dist/markup.d.ts +39 -0
- package/dist/markup.js +465 -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 +21 -2
- package/dist/pick.js +152 -19
- package/dist/popups.d.ts +10 -0
- package/dist/popups.js +2 -1
- package/dist/postfix.d.ts +21 -0
- package/dist/postfix.js +212 -0
- package/dist/project-language.d.ts +20 -1
- package/dist/project-language.js +132 -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 +24 -0
- package/dist/signature.js +3 -6
- package/dist/snippet-insert.d.ts +10 -0
- package/dist/snippet-insert.js +217 -0
- package/dist/snippet-session.d.ts +2 -2
- package/dist/snippet-session.js +3 -3
- package/dist/snippet.d.ts +7 -1
- package/dist/snippet.js +65 -8
- package/dist/surround-constructs.d.ts +10 -0
- package/dist/surround-constructs.js +100 -0
- package/dist/surround.d.ts +18 -0
- package/dist/surround.js +399 -0
- package/dist/symbol-picker.js +4 -6
- package/dist/symbols.js +22 -3
- package/dist/template-builtins.d.ts +3 -0
- package/dist/template-builtins.js +46 -0
- package/dist/template-context.d.ts +12 -0
- package/dist/template-context.js +56 -0
- package/dist/templates.d.ts +59 -0
- package/dist/templates.js +103 -0
- 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/LanguagePopups.tsx +4 -0
- package/src/PickPopup.tsx +47 -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 +123 -25
- 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 +86 -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 +17 -0
- package/src/index.ts +28 -3
- package/src/inlay-hints.ts +2 -2
- package/src/inline-completion.ts +221 -0
- package/src/key-priority.ts +11 -6
- package/src/linked-editing.ts +199 -0
- package/src/live-templates.ts +315 -0
- package/src/locales/en.json +61 -2
- package/src/locales/nl.json +61 -2
- package/src/markup-context.ts +387 -0
- package/src/markup.ts +377 -0
- package/src/navigation.ts +6 -6
- package/src/paste-history.ts +111 -0
- package/src/peek.ts +4 -4
- package/src/pick.ts +218 -20
- package/src/popups.ts +21 -1
- package/src/postfix.ts +224 -0
- package/src/project-language.ts +139 -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-insert.ts +258 -0
- package/src/snippet-session.ts +9 -4
- package/src/snippet.ts +82 -7
- package/src/surround-constructs.ts +85 -0
- package/src/surround.ts +430 -0
- package/src/symbol-picker.ts +6 -6
- package/src/symbols.ts +30 -4
- package/src/template-builtins.ts +94 -0
- package/src/template-context.ts +65 -0
- package/src/templates.ts +197 -0
- 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,15 +367,37 @@ export class PickFeature {
|
|
|
202
367
|
|
|
203
368
|
return true;
|
|
204
369
|
default:
|
|
205
|
-
|
|
370
|
+
if (spec.filter === true && event.key.length === 1) {
|
|
371
|
+
this.requery(this.query + event.key);
|
|
372
|
+
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return spec.filter !== true && event.key.length === 1 && this.takeMnemonic(event.key);
|
|
206
377
|
}
|
|
207
378
|
}
|
|
208
379
|
|
|
380
|
+
/* Takes the visible row whose mnemonic is the typed character, whatever the case. */
|
|
381
|
+
private takeMnemonic(character: string): boolean {
|
|
382
|
+
const wanted = character.toLowerCase();
|
|
383
|
+
const row = this.visibleGroups()
|
|
384
|
+
.flatMap((group) => group.rows)
|
|
385
|
+
.find((candidate) => candidate.mnemonic?.toLowerCase() === wanted);
|
|
386
|
+
|
|
387
|
+
if (row === undefined) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.choose(row.id);
|
|
392
|
+
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
209
396
|
private schedulePreview(): void {
|
|
210
397
|
const spec = this.spec;
|
|
211
398
|
this.timers.clear(this.previewTimer);
|
|
212
399
|
|
|
213
|
-
if (spec?.preview === undefined) {
|
|
400
|
+
if (spec?.preview === undefined || this.active === '') {
|
|
214
401
|
return;
|
|
215
402
|
}
|
|
216
403
|
|
|
@@ -239,6 +426,17 @@ export class PickFeature {
|
|
|
239
426
|
|
|
240
427
|
const refusal = this.refusals.get(this.active);
|
|
241
428
|
const shown = refusal === undefined ? preview : { removed: [], added: [], note: null, refusal };
|
|
242
|
-
|
|
429
|
+
|
|
430
|
+
this.language.popups.setState({
|
|
431
|
+
pick: {
|
|
432
|
+
anchor: spec.anchor,
|
|
433
|
+
groups: this.visibleGroups(),
|
|
434
|
+
active: this.active,
|
|
435
|
+
preview: shown,
|
|
436
|
+
title: spec.title ?? null,
|
|
437
|
+
query: spec.filter === true ? this.query : null,
|
|
438
|
+
chosen: this.chosen
|
|
439
|
+
}
|
|
440
|
+
});
|
|
243
441
|
}
|
|
244
442
|
}
|
package/src/popups.ts
CHANGED
|
@@ -82,6 +82,10 @@ 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;
|
|
87
|
+
/* One character that takes the row when typed in a list that does not filter, such as `1` or `A`. */
|
|
88
|
+
readonly mnemonic?: string;
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
export interface PickGroup {
|
|
@@ -97,15 +101,22 @@ export interface PickPreview {
|
|
|
97
101
|
readonly note: string | null;
|
|
98
102
|
/* Why the action cannot be taken here, which the server said; nothing would change. */
|
|
99
103
|
readonly refusal?: string;
|
|
104
|
+
/* `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. */
|
|
105
|
+
readonly kind?: 'diff' | 'text';
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
/* A list under a character of the editor; Enter takes the active row, Escape closes it. */
|
|
103
109
|
export interface PickView {
|
|
104
110
|
readonly anchor: EditorPosition;
|
|
111
|
+
/* The rows a query lets through. */
|
|
105
112
|
readonly groups: readonly PickGroup[];
|
|
106
113
|
readonly active: string;
|
|
107
114
|
readonly preview: PickPreview | null;
|
|
108
115
|
readonly title: string | null;
|
|
116
|
+
/* What has been typed to narrow the rows; null for a list that does not filter. */
|
|
117
|
+
readonly query: string | null;
|
|
118
|
+
/* 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. */
|
|
119
|
+
readonly chosen: readonly string[];
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
/* One file of a rename in the preview: the lines it changes. */
|
|
@@ -192,6 +203,13 @@ export interface AuthorsView {
|
|
|
192
203
|
readonly openCommit: (() => void) | null;
|
|
193
204
|
}
|
|
194
205
|
|
|
206
|
+
/* The field that asks for the abbreviation a selection is wrapped in. */
|
|
207
|
+
export interface WrapView {
|
|
208
|
+
readonly anchor: EditorPosition;
|
|
209
|
+
/* Why the last abbreviation did not expand; the field stays open for another. */
|
|
210
|
+
readonly error: string | null;
|
|
211
|
+
}
|
|
212
|
+
|
|
195
213
|
export interface PopupState {
|
|
196
214
|
readonly hover: HoverView | null;
|
|
197
215
|
readonly completion: CompletionView | null;
|
|
@@ -203,6 +221,7 @@ export interface PopupState {
|
|
|
203
221
|
readonly symbols: SymbolsView | null;
|
|
204
222
|
readonly menu: MenuView | null;
|
|
205
223
|
readonly authors: AuthorsView | null;
|
|
224
|
+
readonly wrap: WrapView | null;
|
|
206
225
|
}
|
|
207
226
|
|
|
208
227
|
export type PopupStore = StoreApi<PopupState>;
|
|
@@ -218,6 +237,7 @@ export function createPopupStore(): PopupStore {
|
|
|
218
237
|
callHierarchy: null,
|
|
219
238
|
symbols: null,
|
|
220
239
|
menu: null,
|
|
221
|
-
authors: null
|
|
240
|
+
authors: null,
|
|
241
|
+
wrap: null
|
|
222
242
|
}));
|
|
223
243
|
}
|
package/src/postfix.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { EditorPosition, EditorPostfixOperand, EditorRange } from '@adecore/editor';
|
|
2
|
+
import type { CompletionItem } from '@adecore/lsp';
|
|
3
|
+
import type { LocalCompletionItem } from './completion.ts';
|
|
4
|
+
import { isIdentifierCharacter } from './completion-model.ts';
|
|
5
|
+
import { comparePositions } from './diagnostics-model.ts';
|
|
6
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
7
|
+
import { lineTextOf, textBeforeOf } from './line-text.ts';
|
|
8
|
+
import type { PickRow } from './popups.ts';
|
|
9
|
+
import { bodyWithIndentUnit, dedentLines, indentUnitOf, insertSnippet } from './snippet-insert.ts';
|
|
10
|
+
import { compileSnippet } from './snippet.ts';
|
|
11
|
+
import { SNIPPET_KIND, sortAfterServer, templateContextAt } from './template-context.ts';
|
|
12
|
+
import type { PostfixTemplate } from './templates.ts';
|
|
13
|
+
|
|
14
|
+
const MAX_ROW_LENGTH = 80;
|
|
15
|
+
|
|
16
|
+
/* The postfix templates that apply to the expressions before one dot. */
|
|
17
|
+
interface Candidates {
|
|
18
|
+
readonly dot: EditorPosition;
|
|
19
|
+
readonly operands: readonly EditorPostfixOperand[];
|
|
20
|
+
readonly templates: readonly PostfixTemplate[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* A key Tab can expand, starting at the dot that leads it. */
|
|
24
|
+
interface PostfixKey {
|
|
25
|
+
readonly start: EditorPosition;
|
|
26
|
+
expand(): boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* The identifier characters after the last dot of `before`, and where that dot is; null when no dot leads them. */
|
|
30
|
+
function typedKey(before: string): { readonly key: string; readonly dot: number } | null {
|
|
31
|
+
let index = before.length;
|
|
32
|
+
|
|
33
|
+
while (index > 0 && isIdentifierCharacter(before[index - 1]!)) {
|
|
34
|
+
index--;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return index > 0 && before[index - 1] === '.' ? { key: before.slice(index), dot: index - 1 } : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* One line of a chooser row: whitespace collapsed and cut short. */
|
|
41
|
+
function rowLabel(text: string): string {
|
|
42
|
+
const line = text.replace(/\s+/g, ' ').trim();
|
|
43
|
+
|
|
44
|
+
return line.length > MAX_ROW_LENGTH ? `${line.slice(0, MAX_ROW_LENGTH - 1)}…` : line;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
* Postfix completion: a dot and a key after an expression, and Tab or an accepted row wraps the expression in a
|
|
49
|
+
* template. The registry holds the templates and `TemplatesFeature` decides whether Tab is ours or a live template's.
|
|
50
|
+
*/
|
|
51
|
+
export class PostfixFeature {
|
|
52
|
+
private readonly language: EditorLanguage;
|
|
53
|
+
|
|
54
|
+
constructor(language: EditorLanguage) {
|
|
55
|
+
this.language = language;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* The key before the caret that a template of the registry applies to; null where Tab has no postfix template to expand. */
|
|
59
|
+
key(): PostfixKey | null {
|
|
60
|
+
const { editor, project } = this.language;
|
|
61
|
+
|
|
62
|
+
if (!project.templates.settings.postfix) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const caret = editor.getCaret();
|
|
67
|
+
const typed = typedKey(textBeforeOf(editor, caret));
|
|
68
|
+
|
|
69
|
+
if (typed === null || typed.key === '') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const candidates = this.candidates({ line: caret.line, character: typed.dot }, (template) => template.key === typed.key);
|
|
74
|
+
|
|
75
|
+
if (candidates === null) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { start: candidates.dot, expand: () => this.expandKey(candidates, caret) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* The rows of the suggestion list after a dot, for the templates whose key the typed characters can lead to. */
|
|
83
|
+
rows(position: EditorPosition): readonly LocalCompletionItem[] {
|
|
84
|
+
const { editor, project } = this.language;
|
|
85
|
+
const { settings } = project.templates;
|
|
86
|
+
const selections = editor.getSelections();
|
|
87
|
+
|
|
88
|
+
if (!settings.postfix || !settings.postfixInCompletion || selections.length !== 1 || comparePositions(selections[0]!.start, selections[0]!.end) !== 0) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const typed = typedKey(textBeforeOf(editor, position));
|
|
93
|
+
|
|
94
|
+
if (typed === null) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const candidates = this.candidates({ line: position.line, character: typed.dot }, (template) => template.key.startsWith(typed.key));
|
|
99
|
+
|
|
100
|
+
if (candidates === null) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const range = { start: { line: position.line, character: typed.dot + 1 }, end: position };
|
|
105
|
+
|
|
106
|
+
return candidates.templates.map((template) => this.row(template, candidates, range, sortAfterServer(template.key)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private row(template: PostfixTemplate, candidates: Candidates, range: EditorRange, sortText: string): LocalCompletionItem {
|
|
110
|
+
const item: CompletionItem = {
|
|
111
|
+
label: template.key,
|
|
112
|
+
kind: SNIPPET_KIND,
|
|
113
|
+
detail: template.example,
|
|
114
|
+
documentation: template.description,
|
|
115
|
+
filterText: template.key,
|
|
116
|
+
sortText,
|
|
117
|
+
textEdit: { range, newText: template.key }
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return { item, accept: () => this.expand(template, candidates, this.language.editor.getCaret()) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* The templates `matches` lets through that apply to the expressions before `dot`; null when none does. */
|
|
124
|
+
private candidates(dot: EditorPosition, matches: (template: PostfixTemplate) => boolean): Candidates | null {
|
|
125
|
+
const { editor, project } = this.language;
|
|
126
|
+
const operands = editor.postfixOperands?.(dot) ?? [];
|
|
127
|
+
|
|
128
|
+
if (operands.length === 0) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const context = templateContextAt(this.language, dot);
|
|
133
|
+
|
|
134
|
+
if (context.place !== 'code') {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const widest = operands.at(-1)!;
|
|
139
|
+
|
|
140
|
+
const templates = project.templates.postfix.filter(
|
|
141
|
+
(template) =>
|
|
142
|
+
matches(template) &&
|
|
143
|
+
template.languages.some((family) => context.families.includes(family)) &&
|
|
144
|
+
(template.expression !== 'statement' || widest.statement)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return templates.length === 0 ? null : { dot, operands, templates };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Expands the one template of a key, or lists them when several share it. */
|
|
151
|
+
private expandKey(candidates: Candidates, caret: EditorPosition): boolean {
|
|
152
|
+
if (candidates.templates.length === 1) {
|
|
153
|
+
this.expand(candidates.templates[0]!, candidates, caret);
|
|
154
|
+
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const start = { line: candidates.dot.line, character: candidates.dot.character + 1 };
|
|
159
|
+
|
|
160
|
+
this.language.completion.showItems(
|
|
161
|
+
candidates.templates.map((template) => this.row(template, candidates, { start, end: caret }, template.id)),
|
|
162
|
+
start
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Wraps the widest expression for a statement, the only one there is, or the one a person picks from the expressions. */
|
|
169
|
+
private expand(template: PostfixTemplate, candidates: Candidates, caret: EditorPosition): void {
|
|
170
|
+
const { operands } = candidates;
|
|
171
|
+
|
|
172
|
+
if (template.expression === 'statement' || operands.length === 1) {
|
|
173
|
+
void this.run(template, template.expression === 'statement' ? operands.at(-1)! : operands[0]!, caret);
|
|
174
|
+
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const { editor, completion, pick, project } = this.language;
|
|
179
|
+
const rows = operands.map((operand, index): PickRow => ({ id: String(index), label: rowLabel(editor.textInRange(operand.range)), detail: '' }));
|
|
180
|
+
|
|
181
|
+
completion.close();
|
|
182
|
+
|
|
183
|
+
pick.open({
|
|
184
|
+
anchor: caret,
|
|
185
|
+
title: project.i18n.t('editor:language.postfix.expressions'),
|
|
186
|
+
groups: [{ title: null, rows }],
|
|
187
|
+
accept: (id) => void this.run(template, operands[Number(id)]!, caret),
|
|
188
|
+
preview: (id) => Promise.resolve({ removed: [], added: this.rendered(template, operands[Number(id)]!).split('\n'), note: null, kind: 'text' })
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private run(template: PostfixTemplate, operand: EditorPostfixOperand, caret: EditorPosition): Promise<boolean> {
|
|
193
|
+
const { editor } = this.language;
|
|
194
|
+
const expression = this.expressionOf(operand);
|
|
195
|
+
|
|
196
|
+
return insertSnippet(
|
|
197
|
+
this.language,
|
|
198
|
+
bodyWithIndentUnit(template.body, indentUnitOf(editor)),
|
|
199
|
+
[{ range: { start: operand.range.start, end: caret } }],
|
|
200
|
+
(name) => (name === 'EXPR' ? expression : undefined)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* What the template would put in place of the expression, tab stops at their placeholders. */
|
|
205
|
+
private rendered(template: PostfixTemplate, operand: EditorPostfixOperand): string {
|
|
206
|
+
const expression = this.expressionOf(operand);
|
|
207
|
+
|
|
208
|
+
return compileSnippet(bodyWithIndentUnit(template.body, indentUnitOf(this.language.editor))).render(
|
|
209
|
+
(name) => (name === 'EXPR' ? expression : undefined),
|
|
210
|
+
undefined,
|
|
211
|
+
{
|
|
212
|
+
indentVariables: true
|
|
213
|
+
}
|
|
214
|
+
).text;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* The text of an expression, with the indentation of its first line taken off the lines after it. */
|
|
218
|
+
private expressionOf(operand: EditorPostfixOperand): string {
|
|
219
|
+
const { editor } = this.language;
|
|
220
|
+
const indent = /^[\t ]*/.exec(lineTextOf(editor, operand.range.start.line))![0];
|
|
221
|
+
|
|
222
|
+
return dedentLines(editor.textInRange(operand.range).trimEnd(), indent);
|
|
223
|
+
}
|
|
224
|
+
}
|