@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.
Files changed (154) hide show
  1. package/dist/CallHierarchyPanel.js +30 -55
  2. package/dist/CodeAuthorsCard.js +0 -1
  3. package/dist/CompletionPopup.js +150 -181
  4. package/dist/DiffView.d.ts +1 -1
  5. package/dist/DiffView.js +2 -29
  6. package/dist/PickPopup.js +54 -80
  7. package/dist/SplitDiffView.js +27 -57
  8. package/dist/SymbolPicker.js +2 -8
  9. package/dist/code-actions-model.js +2 -9
  10. package/dist/code-actions.js +10 -11
  11. package/dist/code-vision.js +2 -1
  12. package/dist/completion-model.js +2 -2
  13. package/dist/completion.js +13 -34
  14. package/dist/definition-link.d.ts +1 -0
  15. package/dist/definition-link.js +22 -18
  16. package/dist/diagnostics-model.d.ts +1 -0
  17. package/dist/diagnostics-model.js +3 -1
  18. package/dist/diagnostics.js +1 -2
  19. package/dist/diff-syntax.d.ts +1 -0
  20. package/dist/diff-syntax.js +31 -0
  21. package/dist/doc-comment-model.d.ts +1 -0
  22. package/dist/doc-comment-model.js +1 -1
  23. package/dist/folding.js +2 -3
  24. package/dist/format-on-type.js +6 -10
  25. package/dist/hover-content.d.ts +0 -2
  26. package/dist/hover-content.js +1 -9
  27. package/dist/inlay-hints.d.ts +0 -1
  28. package/dist/inlay-hints.js +11 -41
  29. package/dist/navigation.js +4 -14
  30. package/dist/peek.js +2 -12
  31. package/dist/popup-focus.d.ts +3 -1
  32. package/dist/popup-focus.js +33 -0
  33. package/dist/recent-choices.d.ts +0 -1
  34. package/dist/recent-choices.js +0 -4
  35. package/dist/rendered-docs.d.ts +1 -0
  36. package/dist/rendered-docs.js +15 -28
  37. package/dist/snippet-field-transforms.d.ts +5 -0
  38. package/dist/snippet-field-transforms.js +27 -16
  39. package/dist/snippet-session.js +8 -20
  40. package/dist/symbol-links.js +3 -10
  41. package/dist/symbol-picker-model.d.ts +1 -0
  42. package/dist/symbol-picker-model.js +7 -0
  43. package/dist/unique.d.ts +10 -0
  44. package/dist/unique.js +15 -0
  45. package/package.json +9 -9
  46. package/src/AnchoredPopup.tsx +2 -0
  47. package/src/BaselineStatus.tsx +1 -0
  48. package/src/CallHierarchyPanel.tsx +16 -6
  49. package/src/ChangeReview.tsx +7 -0
  50. package/src/CodeAuthorsCard.tsx +2 -5
  51. package/src/CompletionPopup.tsx +21 -15
  52. package/src/DiffView.tsx +53 -30
  53. package/src/EditorContextMenu.tsx +7 -0
  54. package/src/EditorView.tsx +28 -0
  55. package/src/FindReplace.tsx +8 -0
  56. package/src/HoverCard.tsx +8 -0
  57. package/src/HoverSections.tsx +11 -0
  58. package/src/LanguagePopups.tsx +1 -0
  59. package/src/PanelResizeHandle.tsx +8 -0
  60. package/src/PathText.tsx +1 -0
  61. package/src/PeekPanel.tsx +13 -0
  62. package/src/PickPopup.tsx +7 -6
  63. package/src/RenameCard.tsx +13 -0
  64. package/src/RenderedDocs.tsx +11 -0
  65. package/src/SplitDiffView.tsx +91 -56
  66. package/src/SymbolPicker.tsx +10 -18
  67. package/src/call-hierarchy-model.ts +10 -0
  68. package/src/call-hierarchy.ts +82 -0
  69. package/src/code-actions-model.ts +21 -9
  70. package/src/code-actions.ts +83 -7
  71. package/src/code-authors.ts +20 -0
  72. package/src/code-vision-model.ts +37 -0
  73. package/src/code-vision.ts +47 -3
  74. package/src/completion-call.ts +19 -0
  75. package/src/completion-model.ts +58 -2
  76. package/src/completion.ts +156 -30
  77. package/src/conflict-model.ts +4 -0
  78. package/src/context-menu-rows.ts +1 -0
  79. package/src/context-menu.ts +7 -0
  80. package/src/definition-link.ts +42 -18
  81. package/src/diagnostics-model.ts +19 -1
  82. package/src/diagnostics.ts +31 -2
  83. package/src/diff-expansion.ts +9 -0
  84. package/src/diff-overlays.tsx +8 -0
  85. package/src/diff-syntax.ts +54 -0
  86. package/src/diff-wordmarks.ts +8 -0
  87. package/src/doc-comment-model.ts +51 -1
  88. package/src/editor-language.ts +11 -0
  89. package/src/find-query.ts +7 -0
  90. package/src/folding.ts +10 -3
  91. package/src/format-on-type.ts +23 -10
  92. package/src/highlight-layers.ts +3 -0
  93. package/src/highlights.ts +18 -0
  94. package/src/history.ts +11 -0
  95. package/src/holder.ts +3 -0
  96. package/src/hover-content.ts +34 -17
  97. package/src/hover.ts +57 -0
  98. package/src/index.ts +4 -0
  99. package/src/inlay-hints.ts +86 -47
  100. package/src/key-priority.ts +2 -0
  101. package/src/line-edits.ts +8 -0
  102. package/src/models.ts +6 -0
  103. package/src/navigation-history.ts +5 -0
  104. package/src/navigation.ts +28 -13
  105. package/src/panel-size.ts +7 -0
  106. package/src/patch-decorations.ts +39 -0
  107. package/src/patch-layout.ts +47 -0
  108. package/src/peek-model.ts +21 -0
  109. package/src/peek.ts +49 -13
  110. package/src/pick.ts +35 -0
  111. package/src/popup-focus.ts +11 -1
  112. package/src/popup-placement.ts +5 -0
  113. package/src/popups.ts +2 -6
  114. package/src/project-language.ts +33 -0
  115. package/src/project-problems.ts +11 -0
  116. package/src/proposal-model.ts +40 -0
  117. package/src/recent-choices.ts +3 -5
  118. package/src/refresher.ts +3 -0
  119. package/src/rename-model.ts +28 -0
  120. package/src/rename.ts +56 -0
  121. package/src/rendered-docs.ts +89 -19
  122. package/src/review-group.ts +4 -0
  123. package/src/row-host.ts +12 -0
  124. package/src/selection-ranges.ts +6 -0
  125. package/src/semantic-delta.ts +7 -0
  126. package/src/semantic-model.ts +11 -0
  127. package/src/semantic-tokens.ts +37 -0
  128. package/src/shortcut-keys.ts +3 -0
  129. package/src/signature-model.ts +7 -0
  130. package/src/signature.ts +27 -0
  131. package/src/snippet-field-transforms.ts +48 -13
  132. package/src/snippet-indent.ts +10 -0
  133. package/src/snippet-range-order.ts +5 -0
  134. package/src/snippet-session.ts +173 -31
  135. package/src/snippet-transform-client.ts +14 -0
  136. package/src/snippet-transform-format.ts +98 -0
  137. package/src/snippet-transform-protocol.ts +5 -0
  138. package/src/snippet-transform-queue.ts +53 -0
  139. package/src/snippet-transform-worker.ts +14 -0
  140. package/src/snippet-variables.ts +9 -0
  141. package/src/snippet.ts +107 -0
  142. package/src/split-alignment.ts +10 -0
  143. package/src/split-scroll.ts +8 -0
  144. package/src/symbol-links.ts +9 -12
  145. package/src/symbol-picker-model.ts +25 -0
  146. package/src/symbol-picker.ts +9 -0
  147. package/src/symbols.ts +21 -0
  148. package/src/testing.ts +7 -0
  149. package/src/timers.ts +5 -0
  150. package/src/unique.ts +23 -0
  151. package/src/use-problem-counts.ts +4 -0
  152. package/src/view-state.ts +9 -0
  153. package/src/word-diff.ts +5 -0
  154. 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, type SnippetTransformJob } from './snippet-transform-protocol.ts';
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
- return (
206
- selections.length === acceptedSelections.length &&
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(item.textEdit?.newText ?? item.insertText ?? item.label) : undefined;
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.textInRange({ start: { line: position.line, character: 0 }, end: { line: position.line, character: Number.MAX_SAFE_INTEGER } });
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) => comparePositions(range.start, selection.start) === 0 && comparePositions(range.end, selection.end) === 0)
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: SnippetTransformJob[] = [];
335
- const keys = new Map<string, number>();
336
- const slots = drafts.map((draft) =>
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
  }