@adecore/editor-react 0.39.0 → 0.40.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 +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/completion.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { SnippetTransformQueueError } from './snippet-transform-queue.ts';
|
|
2
2
|
import { KEY_PRIORITY } from './key-priority.ts';
|
|
3
3
|
import { SnippetTransformError } from './snippet-transform-client.ts';
|
|
4
|
-
import { MAX_TRANSFORM_JOBS, MAX_TRANSFORM_WORK
|
|
4
|
+
import { MAX_TRANSFORM_JOBS, MAX_TRANSFORM_WORK } from './snippet-transform-protocol.ts';
|
|
5
|
+
import { jobTable } from './snippet-field-transforms.ts';
|
|
5
6
|
import { fileUriToPath, StaleResultError, type CompletionContext, type CompletionItem } from '@adecore/lsp';
|
|
6
7
|
import type { EditorContentChange, EditorPosition, EditorRange, EditorTextChange } from '@adecore/editor';
|
|
7
|
-
import { comparePositions, shiftPosition } from './diagnostics-model.ts';
|
|
8
|
+
import { comparePositions, sameRange, shiftPosition } from './diagnostics-model.ts';
|
|
8
9
|
import type { EditorLanguage } from './editor-language.ts';
|
|
9
10
|
import {
|
|
10
11
|
completionDocsOf,
|
|
@@ -28,7 +29,7 @@ import { createSnippetResolver } from './snippet-variables.ts';
|
|
|
28
29
|
import { wordRangeAt } from './rename-model.ts';
|
|
29
30
|
import { RecentChoices, recentChoicesOf } from './recent-choices.ts';
|
|
30
31
|
import { shikiLanguageOf } from './language-ids.ts';
|
|
31
|
-
import { textBeforeOf } from './line-text.ts';
|
|
32
|
+
import { lineTextOf, textBeforeOf } from './line-text.ts';
|
|
32
33
|
import { isEmptyRange } from './proposal-model.ts';
|
|
33
34
|
import { isShortcut } from './shortcut-keys.ts';
|
|
34
35
|
import type { CompletionRow, CompletionView } from './popups.ts';
|
|
@@ -54,21 +55,29 @@ function mapCompletionEdits(
|
|
|
54
55
|
// Changes apply one after another, so each is checked against the range as the earlier ones left it.
|
|
55
56
|
const overlapsChange = (range: EditorRange): boolean => {
|
|
56
57
|
let { start, end } = range;
|
|
58
|
+
|
|
57
59
|
for (const change of changes) {
|
|
58
60
|
if (comparePositions(start, change.range.end) < 0 && comparePositions(change.range.start, end) < 0) {
|
|
59
61
|
return true;
|
|
60
62
|
}
|
|
63
|
+
|
|
61
64
|
start = shiftPosition(start, change);
|
|
62
65
|
end = shiftPosition(end, change);
|
|
63
66
|
}
|
|
67
|
+
|
|
64
68
|
return false;
|
|
65
69
|
};
|
|
70
|
+
|
|
66
71
|
let valid = true;
|
|
72
|
+
|
|
67
73
|
const map = (range: EditorRange): EditorRange => {
|
|
68
74
|
valid &&= !overlapsChange(range);
|
|
75
|
+
|
|
69
76
|
return mapRange(range);
|
|
70
77
|
};
|
|
78
|
+
|
|
71
79
|
const edit = item.textEdit;
|
|
80
|
+
|
|
72
81
|
const mapped = {
|
|
73
82
|
...item,
|
|
74
83
|
...(edit === undefined
|
|
@@ -78,6 +87,7 @@ function mapCompletionEdits(
|
|
|
78
87
|
? {}
|
|
79
88
|
: { additionalTextEdits: item.additionalTextEdits.map((edit) => ({ ...edit, range: map(edit.range) })) })
|
|
80
89
|
};
|
|
90
|
+
|
|
81
91
|
return valid ? mapped : null;
|
|
82
92
|
}
|
|
83
93
|
|
|
@@ -93,11 +103,15 @@ export class CompletionFeature {
|
|
|
93
103
|
private incomplete = false;
|
|
94
104
|
private ranked: readonly Ranked[] = [];
|
|
95
105
|
private active = 0;
|
|
106
|
+
|
|
96
107
|
/* Whether the person moved in the list; until then the active row is the best match, whatever the typing does to the list. */
|
|
97
108
|
private touched = false;
|
|
109
|
+
|
|
98
110
|
private activeKey: string | null = null;
|
|
111
|
+
|
|
99
112
|
/* Asked for with the keyboard, which is as good as moving in the list when a commit character is typed. */
|
|
100
113
|
private explicit = false;
|
|
114
|
+
|
|
101
115
|
private detailsOpen = true;
|
|
102
116
|
private start: EditorPosition | null = null;
|
|
103
117
|
private server = '';
|
|
@@ -105,8 +119,10 @@ export class CompletionFeature {
|
|
|
105
119
|
private controller: AbortController | null = null;
|
|
106
120
|
private resolving: AbortController | null = null;
|
|
107
121
|
private accepting: AbortController | null = null;
|
|
122
|
+
|
|
108
123
|
/* What the list was ranked for last: the caret event and the text event of one keystroke both refilter, and only the first has to rank. */
|
|
109
124
|
private rankedFor: { readonly pool: readonly CompletionItem[]; readonly key: string } | null = null;
|
|
125
|
+
|
|
110
126
|
private requestTimer: unknown;
|
|
111
127
|
private resolveTimer: unknown;
|
|
112
128
|
private textRevision = 0;
|
|
@@ -118,6 +134,7 @@ export class CompletionFeature {
|
|
|
118
134
|
this.language = language;
|
|
119
135
|
this.timers = timers;
|
|
120
136
|
const { editor } = language;
|
|
137
|
+
|
|
121
138
|
const offs = [
|
|
122
139
|
language.onKey((event) => this.key(event), KEY_PRIORITY.completion),
|
|
123
140
|
editor.onTextChange((change) => this.edited(change)),
|
|
@@ -128,12 +145,15 @@ export class CompletionFeature {
|
|
|
128
145
|
this.close();
|
|
129
146
|
})
|
|
130
147
|
];
|
|
148
|
+
|
|
131
149
|
language.onDispose(() => {
|
|
132
150
|
this.disposed = true;
|
|
133
151
|
this.accepting?.abort();
|
|
152
|
+
|
|
134
153
|
for (const off of offs) {
|
|
135
154
|
off();
|
|
136
155
|
}
|
|
156
|
+
|
|
137
157
|
this.close();
|
|
138
158
|
});
|
|
139
159
|
}
|
|
@@ -153,6 +173,7 @@ export class CompletionFeature {
|
|
|
153
173
|
if (this.ranked.length === 0) {
|
|
154
174
|
return;
|
|
155
175
|
}
|
|
176
|
+
|
|
156
177
|
const last = this.ranked.length - 1;
|
|
157
178
|
// One step past either end goes round, a page does not.
|
|
158
179
|
this.active = Math.abs(delta) === 1 ? (this.active + delta + last + 1) % (last + 1) : Math.max(0, Math.min(last, this.active + delta));
|
|
@@ -173,9 +194,11 @@ export class CompletionFeature {
|
|
|
173
194
|
|
|
174
195
|
private async insert(replace: boolean, index: number, commit: string | undefined): Promise<void> {
|
|
175
196
|
const entry = this.ranked[index];
|
|
197
|
+
|
|
176
198
|
if (entry === undefined) {
|
|
177
199
|
return;
|
|
178
200
|
}
|
|
201
|
+
|
|
179
202
|
const { editor, project, uri, snippets } = this.language;
|
|
180
203
|
const acceptSequence = ++this.acceptSequence;
|
|
181
204
|
this.accepting?.abort();
|
|
@@ -184,8 +207,10 @@ export class CompletionFeature {
|
|
|
184
207
|
const snippetToken = snippets.token;
|
|
185
208
|
const inSnippet = snippets.isActive;
|
|
186
209
|
const exitChanges: { changes: readonly EditorContentChange[]; map: (range: EditorRange) => EditorRange }[] = [];
|
|
210
|
+
|
|
187
211
|
if (snippets.hasPendingTransforms) {
|
|
188
212
|
this.close();
|
|
213
|
+
|
|
189
214
|
if (
|
|
190
215
|
!(await snippets.settleTransforms(
|
|
191
216
|
() => !this.disposed && acceptSequence === this.acceptSequence && snippets.token === snippetToken,
|
|
@@ -195,57 +220,66 @@ export class CompletionFeature {
|
|
|
195
220
|
return;
|
|
196
221
|
}
|
|
197
222
|
}
|
|
223
|
+
|
|
198
224
|
const acceptedSelections = editor.getSelections();
|
|
199
225
|
const revision = this.textRevision;
|
|
226
|
+
|
|
200
227
|
const current = (): boolean => {
|
|
201
228
|
if (this.disposed || acceptSequence !== this.acceptSequence || revision !== this.textRevision || (inSnippet && snippets.token !== snippetToken)) {
|
|
202
229
|
return false;
|
|
203
230
|
}
|
|
231
|
+
|
|
204
232
|
const selections = editor.getSelections();
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
selections.every(
|
|
208
|
-
(range, index) =>
|
|
209
|
-
comparePositions(range.start, acceptedSelections[index]!.start) === 0 &&
|
|
210
|
-
comparePositions(range.end, acceptedSelections[index]!.end) === 0
|
|
211
|
-
)
|
|
212
|
-
);
|
|
233
|
+
|
|
234
|
+
return selections.length === acceptedSelections.length && selections.every((range, index) => sameRange(range, acceptedSelections[index]!));
|
|
213
235
|
};
|
|
236
|
+
|
|
214
237
|
recentChoicesOf(this.language.languageId).record(entry.item);
|
|
215
238
|
let item = this.resolved.get(entry.item) ?? entry.item;
|
|
216
239
|
const needsResolve = !this.resolved.has(entry.item) && project.service.providerOptions('completionItem/resolve', uri) !== undefined;
|
|
217
240
|
this.close();
|
|
241
|
+
|
|
218
242
|
if (needsResolve) {
|
|
219
243
|
// Resolving can add the import an item needs; an item that cannot be resolved is still inserted as it is.
|
|
220
244
|
item = await this.resolveNow(entry.item, accepting.signal);
|
|
221
245
|
}
|
|
246
|
+
|
|
222
247
|
if (!current()) {
|
|
223
248
|
return;
|
|
224
249
|
}
|
|
250
|
+
|
|
225
251
|
for (const { changes, map } of exitChanges) {
|
|
226
252
|
const mapped = mapCompletionEdits(item, changes, map);
|
|
253
|
+
|
|
227
254
|
if (mapped === null) {
|
|
228
255
|
return;
|
|
229
256
|
}
|
|
257
|
+
|
|
230
258
|
item = mapped;
|
|
231
259
|
}
|
|
260
|
+
|
|
232
261
|
const caret = editor.getCaret();
|
|
233
262
|
const lineBefore = textBeforeOf(editor, caret);
|
|
234
263
|
const lineAfter = editor.textInRange({ start: caret, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } });
|
|
235
264
|
const carets = editor.getSelections();
|
|
236
265
|
const source = item.textEdit?.newText ?? item.insertText ?? item.label;
|
|
266
|
+
|
|
237
267
|
if (source.length * carets.length > MAX_COMPLETION_CHARACTERS) {
|
|
238
268
|
this.refuseLargeCompletion();
|
|
269
|
+
|
|
239
270
|
return;
|
|
240
271
|
}
|
|
272
|
+
|
|
241
273
|
let characters = 0;
|
|
242
274
|
let ranges = 0;
|
|
243
275
|
const now = new Date();
|
|
244
|
-
const template = item.insertTextFormat === 2 ? compileSnippet(
|
|
276
|
+
const template = item.insertTextFormat === 2 ? compileSnippet(source) : undefined;
|
|
245
277
|
const finishes = new Map<Insertion, (values: readonly string[]) => Insertion>();
|
|
278
|
+
|
|
246
279
|
const renderAt = (position: EditorPosition, selection: EditorRange): Insertion => {
|
|
247
|
-
const line = editor
|
|
280
|
+
const line = lineTextOf(editor, position.line);
|
|
248
281
|
const word = wordRangeAt(line, position);
|
|
282
|
+
|
|
249
283
|
const resolver = createSnippetResolver(
|
|
250
284
|
{
|
|
251
285
|
uri,
|
|
@@ -257,7 +291,7 @@ export class CompletionFeature {
|
|
|
257
291
|
currentWord: word === null ? '' : editor.textInRange(word),
|
|
258
292
|
cursorIndex: Math.max(
|
|
259
293
|
0,
|
|
260
|
-
carets.findIndex((range) =>
|
|
294
|
+
carets.findIndex((range) => sameRange(range, selection))
|
|
261
295
|
),
|
|
262
296
|
cursorCount: carets.length,
|
|
263
297
|
folder: project.host.folder,
|
|
@@ -266,11 +300,14 @@ export class CompletionFeature {
|
|
|
266
300
|
project.host.resolveSnippetVariable?.bind(project.host),
|
|
267
301
|
{ now }
|
|
268
302
|
);
|
|
303
|
+
|
|
269
304
|
const atItem =
|
|
270
305
|
position === caret || item.textEdit === undefined
|
|
271
306
|
? item
|
|
272
307
|
: { ...item, textEdit: undefined, insertText: item.textEdit?.newText ?? item.insertText };
|
|
308
|
+
|
|
273
309
|
const editStart = editRangeOf(atItem, replace)?.start;
|
|
310
|
+
|
|
274
311
|
// A commit character follows the name, so accepting must not also add a call.
|
|
275
312
|
const finish = (variableTransforms?: readonly string[]): Insertion => {
|
|
276
313
|
const insertion = insertionOf(
|
|
@@ -283,32 +320,40 @@ export class CompletionFeature {
|
|
|
283
320
|
template,
|
|
284
321
|
variableTransforms,
|
|
285
322
|
resolve: resolver,
|
|
286
|
-
indentationBefore:
|
|
287
|
-
editStart === undefined ? undefined : editor.textInRange({ start: { line: editStart.line, character: 0 }, end: editStart })
|
|
323
|
+
indentationBefore: editStart === undefined ? undefined : textBeforeOf(editor, editStart)
|
|
288
324
|
}
|
|
289
325
|
);
|
|
326
|
+
|
|
290
327
|
return commit === undefined ? insertion : { ...insertion, text: insertion.text + commit };
|
|
291
328
|
};
|
|
329
|
+
|
|
292
330
|
const rendered = finish();
|
|
293
331
|
finishes.set(rendered, finish);
|
|
294
332
|
characters += rendered.text.length;
|
|
333
|
+
|
|
295
334
|
ranges +=
|
|
296
335
|
(rendered.variableTransforms?.length ?? 0) +
|
|
297
336
|
(rendered.fields?.reduce((count, field) => count + field.ranges.length + (field.linked?.length ?? 0), 1) ?? rendered.stops.length);
|
|
337
|
+
|
|
298
338
|
if (rendered.limited || characters > MAX_COMPLETION_CHARACTERS || ranges > MAX_COMPLETION_RANGES) {
|
|
299
339
|
throw new CompletionLimitError();
|
|
300
340
|
}
|
|
341
|
+
|
|
301
342
|
return rendered;
|
|
302
343
|
};
|
|
344
|
+
|
|
303
345
|
try {
|
|
304
346
|
const planned = renderAt(caret, editor.getSelection());
|
|
305
347
|
const { before, rest } = this.surroundings(planned, caret, lineAfter);
|
|
348
|
+
|
|
306
349
|
const plan: CallPlan =
|
|
307
350
|
commit === undefined ? planCall({ item, languageId: this.language.languageId, insertion: planned, before, rest, replace }) : { action: 'none' };
|
|
351
|
+
|
|
308
352
|
let main = plan.action === 'add' ? withParentheses(planned, plan.inside) : planned;
|
|
309
353
|
const extras = (item.additionalTextEdits ?? []).map((edit): EditorContentChange => ({ range: edit.range, text: edit.newText }));
|
|
310
354
|
const instances = new Map<EditorContentChange, Insertion>();
|
|
311
355
|
const continuing = inSnippet && snippets.isActive && snippetToken === snippets.token;
|
|
356
|
+
|
|
312
357
|
const mirrored = mirroredInsertions(carets, editor.getSelection(), caret, (range) => editor.textInRange(range), main, continuing).map(
|
|
313
358
|
(insertion) => {
|
|
314
359
|
const selection = carets.find(
|
|
@@ -317,6 +362,7 @@ export class CompletionFeature {
|
|
|
317
362
|
range.start.character >= insertion.range.start.character &&
|
|
318
363
|
range.start.character <= insertion.range.end.character
|
|
319
364
|
)!;
|
|
365
|
+
|
|
320
366
|
const position = selection[continuing ? 'end' : 'start'];
|
|
321
367
|
const own = renderAt(position, selection);
|
|
322
368
|
const rendered = plan.action === 'add' ? withParentheses(own, plan.inside) : own;
|
|
@@ -324,57 +370,58 @@ export class CompletionFeature {
|
|
|
324
370
|
const instance = { ...rendered, range: insertion.range };
|
|
325
371
|
instances.set(edit, instance);
|
|
326
372
|
const finish = finishes.get(own);
|
|
373
|
+
|
|
327
374
|
if (finish !== undefined) {
|
|
328
375
|
finishes.set(instance, (values) => ({ ...finish(values), range: insertion.range }));
|
|
329
376
|
}
|
|
377
|
+
|
|
330
378
|
return edit;
|
|
331
379
|
}
|
|
332
380
|
);
|
|
381
|
+
|
|
333
382
|
const drafts = [...mirrored.map((edit) => instances.get(edit)!), main];
|
|
334
|
-
const jobs
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
(draft.variableTransforms ?? []).map((job) => {
|
|
338
|
-
const key = JSON.stringify(job);
|
|
339
|
-
let slot = keys.get(key);
|
|
340
|
-
if (slot === undefined) {
|
|
341
|
-
slot = jobs.length;
|
|
342
|
-
keys.set(key, slot);
|
|
343
|
-
jobs.push(job);
|
|
344
|
-
}
|
|
345
|
-
return slot;
|
|
346
|
-
})
|
|
347
|
-
);
|
|
383
|
+
const { jobs, slotOf } = jobTable();
|
|
384
|
+
const slots = drafts.map((draft) => (draft.variableTransforms ?? []).map(slotOf));
|
|
385
|
+
|
|
348
386
|
if (jobs.length > MAX_TRANSFORM_JOBS || jobs.reduce((sum, job) => sum + job.value.length, 0) > MAX_TRANSFORM_WORK) {
|
|
349
387
|
throw new CompletionLimitError();
|
|
350
388
|
}
|
|
389
|
+
|
|
351
390
|
if (jobs.length > 0 && project.host.snippetTransformWorker !== undefined) {
|
|
352
391
|
const values = await snippets.transformVariables(jobs, current);
|
|
392
|
+
|
|
353
393
|
if (!current()) {
|
|
354
394
|
return;
|
|
355
395
|
}
|
|
396
|
+
|
|
356
397
|
if (values !== null) {
|
|
357
398
|
const transformed = drafts.map((draft, index) =>
|
|
358
399
|
slots[index]!.length === 0 ? draft : finishes.get(draft)!(slots[index]!.map((slot) => values[slot]!))
|
|
359
400
|
);
|
|
401
|
+
|
|
360
402
|
if (
|
|
361
403
|
transformed.some((draft) => draft.limited) ||
|
|
362
404
|
transformed.reduce((sum, draft) => sum + draft.text.length, 0) > MAX_COMPLETION_CHARACTERS
|
|
363
405
|
) {
|
|
364
406
|
throw new CompletionLimitError();
|
|
365
407
|
}
|
|
408
|
+
|
|
366
409
|
main = transformed.at(-1)!;
|
|
410
|
+
|
|
367
411
|
mirrored.forEach((edit, index) => {
|
|
368
412
|
edit.text = transformed[index]!.text;
|
|
369
413
|
instances.set(edit, transformed[index]!);
|
|
370
414
|
});
|
|
371
415
|
}
|
|
372
416
|
}
|
|
417
|
+
|
|
373
418
|
const primary = { range: main.range, text: main.text };
|
|
374
419
|
const changes = [primary, ...mirrored, ...extras];
|
|
420
|
+
|
|
375
421
|
if (changes.reduce((total, edit) => total + edit.text.length, 0) > MAX_COMPLETION_CHARACTERS) {
|
|
376
422
|
throw new CompletionLimitError();
|
|
377
423
|
}
|
|
424
|
+
|
|
378
425
|
// Imports and earlier carets move each insertion independently; the primary instance stays last.
|
|
379
426
|
const starts = [...mirrored, primary].map((insertion) =>
|
|
380
427
|
changes
|
|
@@ -382,22 +429,29 @@ export class CompletionFeature {
|
|
|
382
429
|
.sort((left, right) => comparePositions(right.range.start, left.range.start))
|
|
383
430
|
.reduce((place, change) => shiftPosition(place, change), insertion.range.start)
|
|
384
431
|
);
|
|
432
|
+
|
|
385
433
|
const applied = inSnippet ? snippets.applyEdits(changes, snippetToken, extras) : editor.applyEdits(changes);
|
|
434
|
+
|
|
386
435
|
if (!applied) {
|
|
387
436
|
return;
|
|
388
437
|
}
|
|
438
|
+
|
|
389
439
|
const landed = editor.getSelections();
|
|
440
|
+
|
|
390
441
|
if (commit === undefined) {
|
|
391
442
|
this.language.snippets.beginInstances(
|
|
392
443
|
[...mirrored, primary].map((insertion, index) => {
|
|
393
444
|
const rendered = insertion === primary ? main : instances.get(insertion)!;
|
|
445
|
+
|
|
394
446
|
return { start: starts[index]!, text: rendered.text, stops: rendered.stops, fields: rendered.fields };
|
|
395
447
|
}),
|
|
396
448
|
{ nested: continuing && snippets.isActive }
|
|
397
449
|
);
|
|
450
|
+
|
|
398
451
|
if (plan.action === 'add' && plan.inside && mirrored.length > 0 && main.fields === undefined) {
|
|
399
452
|
this.placeMirroredCarets(carets, mirrored, landed);
|
|
400
453
|
}
|
|
454
|
+
|
|
401
455
|
if (plan.action === 'enter') {
|
|
402
456
|
const end = editor.getCaret();
|
|
403
457
|
editor.setCaret({ line: end.line, character: end.character + plan.shift });
|
|
@@ -407,12 +461,14 @@ export class CompletionFeature {
|
|
|
407
461
|
this.beginRequest({ triggerKind: 2, triggerCharacter: commit }, 0);
|
|
408
462
|
}
|
|
409
463
|
}
|
|
464
|
+
|
|
410
465
|
this.afterInsert(item, commit, lineBefore);
|
|
411
466
|
editor.focus();
|
|
412
467
|
} catch (error) {
|
|
413
468
|
if (!current()) {
|
|
414
469
|
return;
|
|
415
470
|
}
|
|
471
|
+
|
|
416
472
|
if (error instanceof SnippetTransformQueueError) {
|
|
417
473
|
project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformBusy') });
|
|
418
474
|
} else if (error instanceof CompletionLimitError || (error instanceof SnippetTransformError && error.kind === 'range')) {
|
|
@@ -433,17 +489,23 @@ export class CompletionFeature {
|
|
|
433
489
|
private placeMirroredCarets(before: readonly EditorRange[], mirrored: readonly EditorContentChange[], landed: readonly EditorRange[]): void {
|
|
434
490
|
const { editor } = this.language;
|
|
435
491
|
const primary = before.length - 1;
|
|
492
|
+
|
|
436
493
|
const selections = landed.map((range, index): EditorRange => {
|
|
437
494
|
const caret = before[index]!.start;
|
|
495
|
+
|
|
438
496
|
const got = mirrored.some(
|
|
439
497
|
(edit) => edit.range.start.line === caret.line && edit.range.start.character <= caret.character && caret.character <= edit.range.end.character
|
|
440
498
|
);
|
|
499
|
+
|
|
441
500
|
if (index === primary || !got) {
|
|
442
501
|
return range;
|
|
443
502
|
}
|
|
503
|
+
|
|
444
504
|
const stop = { line: range.end.line, character: range.end.character - 1 };
|
|
505
|
+
|
|
445
506
|
return { start: stop, end: stop };
|
|
446
507
|
});
|
|
508
|
+
|
|
447
509
|
selections[primary] = editor.getSelection();
|
|
448
510
|
editor.setSelections(selections);
|
|
449
511
|
}
|
|
@@ -454,6 +516,7 @@ export class CompletionFeature {
|
|
|
454
516
|
const { start, end } = insertion.range;
|
|
455
517
|
const before = editor.textInRange({ start: { line: Math.max(0, start.line - LINES_BACK), character: 0 }, end: start });
|
|
456
518
|
const rest = end.line === caret.line ? lineAfter.slice(Math.max(0, end.character - caret.character)) : '';
|
|
519
|
+
|
|
457
520
|
return { before, rest };
|
|
458
521
|
}
|
|
459
522
|
|
|
@@ -462,13 +525,17 @@ export class CompletionFeature {
|
|
|
462
525
|
const { editor, project, uri, signature } = this.language;
|
|
463
526
|
const { start } = editor.getSelection();
|
|
464
527
|
const insideCall = start.character > 0 && editor.textInRange({ start: { line: start.line, character: start.character - 1 }, end: start }) === '(';
|
|
528
|
+
|
|
465
529
|
if (commit === '(' || (insideCall && isCallItem(item, lineBefore))) {
|
|
466
530
|
signature.trigger();
|
|
467
531
|
}
|
|
532
|
+
|
|
468
533
|
const { command } = item;
|
|
534
|
+
|
|
469
535
|
if (command === undefined) {
|
|
470
536
|
return;
|
|
471
537
|
}
|
|
538
|
+
|
|
472
539
|
if (command.command === PARAMETER_HINTS_COMMAND) {
|
|
473
540
|
signature.trigger();
|
|
474
541
|
} else {
|
|
@@ -483,6 +550,7 @@ export class CompletionFeature {
|
|
|
483
550
|
*/
|
|
484
551
|
private async resolveNow(item: CompletionItem, signal: AbortSignal): Promise<CompletionItem> {
|
|
485
552
|
const { editor, project, uri } = this.language;
|
|
553
|
+
|
|
486
554
|
try {
|
|
487
555
|
return { ...item, ...(await project.service.resolveCompletion(uri, item, { signal })) };
|
|
488
556
|
} catch (error) {
|
|
@@ -490,10 +558,12 @@ export class CompletionFeature {
|
|
|
490
558
|
return item;
|
|
491
559
|
}
|
|
492
560
|
}
|
|
561
|
+
|
|
493
562
|
try {
|
|
494
563
|
const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 }, { signal })).items.find(
|
|
495
564
|
(candidate) => candidate.label === item.label && candidate.kind === item.kind
|
|
496
565
|
);
|
|
566
|
+
|
|
497
567
|
return fresh === undefined ? item : { ...fresh, ...(await project.service.resolveCompletion(uri, fresh, { signal }).catch(() => fresh)) };
|
|
498
568
|
} catch {
|
|
499
569
|
return item;
|
|
@@ -514,6 +584,7 @@ export class CompletionFeature {
|
|
|
514
584
|
this.activeKey = null;
|
|
515
585
|
this.start = null;
|
|
516
586
|
this.resolved.clear();
|
|
587
|
+
|
|
517
588
|
if (this.language.popups.getState().completion !== null) {
|
|
518
589
|
this.language.popups.setState({ completion: null });
|
|
519
590
|
}
|
|
@@ -527,37 +598,49 @@ export class CompletionFeature {
|
|
|
527
598
|
} else {
|
|
528
599
|
this.invoke();
|
|
529
600
|
}
|
|
601
|
+
|
|
530
602
|
return true;
|
|
531
603
|
}
|
|
604
|
+
|
|
532
605
|
if (!this.isOpen || event.ctrlKey || event.metaKey || event.altKey) {
|
|
533
606
|
return false;
|
|
534
607
|
}
|
|
608
|
+
|
|
535
609
|
if (this.commits(event)) {
|
|
536
610
|
void this.accept(false, this.active, event.key);
|
|
611
|
+
|
|
537
612
|
return true;
|
|
538
613
|
}
|
|
614
|
+
|
|
539
615
|
switch (event.key) {
|
|
540
616
|
case 'ArrowDown':
|
|
541
617
|
this.move(1);
|
|
618
|
+
|
|
542
619
|
return true;
|
|
543
620
|
case 'ArrowUp':
|
|
544
621
|
this.move(-1);
|
|
622
|
+
|
|
545
623
|
return true;
|
|
546
624
|
case 'PageDown':
|
|
547
625
|
this.move(PAGE);
|
|
626
|
+
|
|
548
627
|
return true;
|
|
549
628
|
case 'PageUp':
|
|
550
629
|
this.move(-PAGE);
|
|
630
|
+
|
|
551
631
|
return true;
|
|
552
632
|
case 'Enter':
|
|
553
633
|
case 'Tab':
|
|
554
634
|
if (event.shiftKey) {
|
|
555
635
|
return false;
|
|
556
636
|
}
|
|
637
|
+
|
|
557
638
|
void this.accept(event.key === 'Tab');
|
|
639
|
+
|
|
558
640
|
return true;
|
|
559
641
|
case 'Escape':
|
|
560
642
|
this.close();
|
|
643
|
+
|
|
561
644
|
return true;
|
|
562
645
|
default:
|
|
563
646
|
return false;
|
|
@@ -569,19 +652,24 @@ export class CompletionFeature {
|
|
|
569
652
|
if ((!this.touched && !this.explicit) || event.key.length !== 1 || event.isComposing) {
|
|
570
653
|
return false;
|
|
571
654
|
}
|
|
655
|
+
|
|
572
656
|
return this.ranked[this.active]?.item.commitCharacters?.includes(event.key) === true;
|
|
573
657
|
}
|
|
574
658
|
|
|
575
659
|
private edited(change: EditorTextChange): void {
|
|
576
660
|
this.textRevision++;
|
|
661
|
+
|
|
577
662
|
if (change.source !== 'input' || change.changes.length !== 1) {
|
|
578
663
|
this.close();
|
|
664
|
+
|
|
579
665
|
return;
|
|
580
666
|
}
|
|
667
|
+
|
|
581
668
|
const { range, text } = change.changes[0]!;
|
|
582
669
|
const triggers = this.triggerCharacters();
|
|
583
670
|
const last = Array.from(text).at(-1) ?? '';
|
|
584
671
|
const typedIdentifier = text !== '' && Array.from(text).every(isIdentifierCharacter);
|
|
672
|
+
|
|
585
673
|
if (text.length === 1 && triggers.includes(last)) {
|
|
586
674
|
this.close();
|
|
587
675
|
this.beginRequest({ triggerKind: 2, triggerCharacter: last }, 0);
|
|
@@ -606,6 +694,7 @@ export class CompletionFeature {
|
|
|
606
694
|
if (this.start === null) {
|
|
607
695
|
return;
|
|
608
696
|
}
|
|
697
|
+
|
|
609
698
|
if (position.line !== this.start.line || position.character < this.start.character) {
|
|
610
699
|
this.close();
|
|
611
700
|
} else {
|
|
@@ -615,14 +704,17 @@ export class CompletionFeature {
|
|
|
615
704
|
|
|
616
705
|
private triggerCharacters(): readonly string[] {
|
|
617
706
|
const { project, uri } = this.language;
|
|
707
|
+
|
|
618
708
|
return project.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
|
|
619
709
|
}
|
|
620
710
|
|
|
621
711
|
private beginRequest(context: CompletionContext, delay: number): void {
|
|
622
712
|
const { project, uri } = this.language;
|
|
713
|
+
|
|
623
714
|
if (!project.service.supports(METHOD, uri)) {
|
|
624
715
|
return;
|
|
625
716
|
}
|
|
717
|
+
|
|
626
718
|
this.timers.clear(this.requestTimer);
|
|
627
719
|
const request = ++this.request;
|
|
628
720
|
this.requestTimer = this.timers.set(() => void this.ask(context, request), delay);
|
|
@@ -635,23 +727,29 @@ export class CompletionFeature {
|
|
|
635
727
|
const controller = new AbortController();
|
|
636
728
|
this.controller = controller;
|
|
637
729
|
let normalized: ReturnType<typeof itemsOf>;
|
|
730
|
+
|
|
638
731
|
try {
|
|
639
732
|
const result = await project.service.completion(uri, position, context, { signal: controller.signal });
|
|
733
|
+
|
|
640
734
|
if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
|
|
641
735
|
return;
|
|
642
736
|
}
|
|
737
|
+
|
|
643
738
|
normalized = itemsOf(result);
|
|
644
739
|
} catch {
|
|
645
740
|
if (request === this.request && this.controller === controller && !controller.signal.aborted) {
|
|
646
741
|
this.close();
|
|
647
742
|
}
|
|
743
|
+
|
|
648
744
|
return;
|
|
649
745
|
}
|
|
746
|
+
|
|
650
747
|
const { items, incomplete } = normalized;
|
|
651
748
|
this.pool = items;
|
|
652
749
|
this.incomplete = incomplete;
|
|
653
750
|
this.resolved.clear();
|
|
654
751
|
this.server = project.host.serverNames?.(uri)[0] ?? '';
|
|
752
|
+
|
|
655
753
|
if (!refresh) {
|
|
656
754
|
const caret = editor.getCaret();
|
|
657
755
|
const lineBefore = textBeforeOf(editor, caret);
|
|
@@ -663,8 +761,10 @@ export class CompletionFeature {
|
|
|
663
761
|
// The list is made again, so the row that was chosen is found again by what it says.
|
|
664
762
|
this.activeKey = RecentChoices.keyOf(this.ranked[this.active]!.item);
|
|
665
763
|
}
|
|
764
|
+
|
|
666
765
|
const asked = this.explicit && !refresh;
|
|
667
766
|
this.refilter();
|
|
767
|
+
|
|
668
768
|
if (asked && !this.isOpen && !this.incomplete) {
|
|
669
769
|
this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.completion.none') });
|
|
670
770
|
} else if (asked && this.ranked.length === 1 && !this.incomplete) {
|
|
@@ -676,41 +776,55 @@ export class CompletionFeature {
|
|
|
676
776
|
private refilter(): void {
|
|
677
777
|
const { editor } = this.language;
|
|
678
778
|
const caret = editor.getCaret();
|
|
779
|
+
|
|
679
780
|
if (this.start === null || caret.line !== this.start.line || caret.character < this.start.character) {
|
|
680
781
|
this.close();
|
|
782
|
+
|
|
681
783
|
return;
|
|
682
784
|
}
|
|
785
|
+
|
|
683
786
|
const lineBefore = textBeforeOf(editor, caret);
|
|
684
787
|
const typedKey = `${caret.line}:${caret.character}:${lineBefore}`;
|
|
788
|
+
|
|
685
789
|
if (this.rankedFor?.pool === this.pool && this.rankedFor.key === typedKey) {
|
|
686
790
|
return;
|
|
687
791
|
}
|
|
792
|
+
|
|
688
793
|
this.rankedFor = { pool: this.pool, key: typedKey };
|
|
689
794
|
// Most items of a list replace the same range, so the text under it is read once and not once per item.
|
|
690
795
|
const typed = new Map<string, string>();
|
|
796
|
+
|
|
691
797
|
const textBetween = (range: EditorRange): string => {
|
|
692
798
|
const key = `${range.start.line}:${range.start.character}:${range.end.line}:${range.end.character}`;
|
|
693
799
|
let text = typed.get(key);
|
|
800
|
+
|
|
694
801
|
if (text === undefined) {
|
|
695
802
|
text = editor.textInRange(range);
|
|
696
803
|
typed.set(key, text);
|
|
697
804
|
}
|
|
805
|
+
|
|
698
806
|
return text;
|
|
699
807
|
};
|
|
808
|
+
|
|
700
809
|
const before = this.ranked[this.active]?.item;
|
|
701
810
|
const recent = recentChoicesOf(this.language.languageId);
|
|
811
|
+
|
|
702
812
|
this.ranked = rankCompletions(
|
|
703
813
|
this.pool,
|
|
704
814
|
(item) => prefixFor(item, caret, lineBefore, textBetween),
|
|
705
815
|
(item) => recent.recency(item)
|
|
706
816
|
);
|
|
817
|
+
|
|
707
818
|
if (this.ranked.length === 0) {
|
|
708
819
|
if (this.incomplete && this.pool.length > 0) {
|
|
709
820
|
this.beginRequest({ triggerKind: 3 }, START_DELAY_MS);
|
|
710
821
|
}
|
|
822
|
+
|
|
711
823
|
this.close();
|
|
824
|
+
|
|
712
825
|
return;
|
|
713
826
|
}
|
|
827
|
+
|
|
714
828
|
const key = this.activeKey ?? (this.touched && before !== undefined ? RecentChoices.keyOf(before) : null);
|
|
715
829
|
this.activeKey = null;
|
|
716
830
|
const kept = key === null ? -1 : this.ranked.findIndex((entry) => RecentChoices.keyOf(entry.item) === key);
|
|
@@ -718,6 +832,7 @@ export class CompletionFeature {
|
|
|
718
832
|
this.touched = this.touched && kept >= 0;
|
|
719
833
|
this.publish();
|
|
720
834
|
this.scheduleResolve();
|
|
835
|
+
|
|
721
836
|
if (this.incomplete) {
|
|
722
837
|
this.beginRequest({ triggerKind: 3 }, START_DELAY_MS);
|
|
723
838
|
}
|
|
@@ -728,17 +843,22 @@ export class CompletionFeature {
|
|
|
728
843
|
this.timers.clear(this.resolveTimer);
|
|
729
844
|
this.resolving?.abort();
|
|
730
845
|
const entry = this.ranked[this.active];
|
|
846
|
+
|
|
731
847
|
if (entry === undefined || this.resolved.has(entry.item) || project.service.providerOptions('completionItem/resolve', uri) === undefined) {
|
|
732
848
|
return;
|
|
733
849
|
}
|
|
850
|
+
|
|
734
851
|
const request = this.request;
|
|
852
|
+
|
|
735
853
|
this.resolveTimer = this.timers.set(() => {
|
|
736
854
|
const controller = new AbortController();
|
|
737
855
|
this.resolving = controller;
|
|
856
|
+
|
|
738
857
|
void project.service
|
|
739
858
|
.resolveCompletion(uri, entry.item, { signal: controller.signal })
|
|
740
859
|
.then((item) => {
|
|
741
860
|
this.resolved.set(entry.item, { ...entry.item, ...item });
|
|
861
|
+
|
|
742
862
|
if (this.ranked[this.active]?.item === entry.item) {
|
|
743
863
|
this.publish();
|
|
744
864
|
}
|
|
@@ -754,11 +874,14 @@ export class CompletionFeature {
|
|
|
754
874
|
|
|
755
875
|
private publish(): void {
|
|
756
876
|
const entry = this.ranked[this.active];
|
|
877
|
+
|
|
757
878
|
if (entry === undefined || this.start === null) {
|
|
758
879
|
return;
|
|
759
880
|
}
|
|
881
|
+
|
|
760
882
|
const item = this.resolved.get(entry.item) ?? entry.item;
|
|
761
883
|
const descriptions = qualifiersOf(this.ranked.map((ranked) => ranked.item));
|
|
884
|
+
|
|
762
885
|
const rows: CompletionRow[] = this.ranked.map(({ item: row, prefix }, index) => ({
|
|
763
886
|
label: row.label,
|
|
764
887
|
kind: row.kind,
|
|
@@ -767,7 +890,9 @@ export class CompletionFeature {
|
|
|
767
890
|
matches: matchedCharacters(row.label, prefix),
|
|
768
891
|
deprecated: row.deprecated === true || row.tags?.includes(1) === true
|
|
769
892
|
}));
|
|
893
|
+
|
|
770
894
|
const highlightLanguage = shikiLanguageOf(this.language.languageId);
|
|
895
|
+
|
|
771
896
|
const view: CompletionView = {
|
|
772
897
|
anchor: this.start,
|
|
773
898
|
rows,
|
|
@@ -777,6 +902,7 @@ export class CompletionFeature {
|
|
|
777
902
|
server: this.server,
|
|
778
903
|
highlightLanguage
|
|
779
904
|
};
|
|
905
|
+
|
|
780
906
|
this.language.popups.setState({ completion: view });
|
|
781
907
|
}
|
|
782
908
|
}
|