@adecore/editor-react 0.31.12 → 0.33.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 (137) hide show
  1. package/dist/AnchoredPopup.js +73 -19
  2. package/dist/AttributionCard.js +162 -4
  3. package/dist/BaselineStatus.js +62 -8
  4. package/dist/CallHierarchyPanel.js +977 -60
  5. package/dist/ChangeReview.js +76 -25
  6. package/dist/CodeAuthorsCard.js +314 -14
  7. package/dist/CompletionPopup.js +490 -83
  8. package/dist/DiffView.js +548 -181
  9. package/dist/EditorContextMenu.js +352 -34
  10. package/dist/EditorView.js +276 -99
  11. package/dist/FindReplace.js +525 -67
  12. package/dist/HoverCard.js +378 -38
  13. package/dist/HoverSections.js +367 -56
  14. package/dist/LanguagePopups.js +95 -34
  15. package/dist/Markdown.js +41 -6
  16. package/dist/PanelResizeHandle.js +199 -66
  17. package/dist/PathText.js +49 -7
  18. package/dist/PeekPanel.js +675 -72
  19. package/dist/PickPopup.js +304 -34
  20. package/dist/RenameCard.js +679 -85
  21. package/dist/RenderedDocs.js +329 -58
  22. package/dist/SignatureCard.js +91 -7
  23. package/dist/SplitDiffView.js +344 -254
  24. package/dist/SymbolPicker.js +440 -85
  25. package/dist/call-hierarchy-model.js +102 -68
  26. package/dist/call-hierarchy.js +458 -359
  27. package/dist/code-actions-model.js +88 -76
  28. package/dist/code-actions.js +372 -283
  29. package/dist/code-authors.js +64 -55
  30. package/dist/code-vision-model.js +134 -127
  31. package/dist/code-vision.js +224 -206
  32. package/dist/completion-call.js +94 -67
  33. package/dist/completion-model.js +334 -244
  34. package/dist/completion.js +840 -649
  35. package/dist/conflict-model.js +35 -29
  36. package/dist/context-menu-rows.js +42 -17
  37. package/dist/context-menu.js +64 -46
  38. package/dist/definition-link.js +88 -85
  39. package/dist/diagnostics-model.js +74 -55
  40. package/dist/diagnostics.js +121 -100
  41. package/dist/diff-expansion.js +38 -35
  42. package/dist/diff-overlays.js +144 -22
  43. package/dist/diff-syntax.js +65 -42
  44. package/dist/diff-wordmarks.js +26 -23
  45. package/dist/doc-comment-model.js +265 -216
  46. package/dist/editor-language.js +177 -152
  47. package/dist/error-message.js +2 -2
  48. package/dist/find-query.js +49 -36
  49. package/dist/folding.js +81 -62
  50. package/dist/format-on-type.js +86 -67
  51. package/dist/highlight-layers.js +19 -20
  52. package/dist/highlights.js +105 -83
  53. package/dist/history.js +44 -38
  54. package/dist/holder.js +18 -18
  55. package/dist/host-types.js +1 -1
  56. package/dist/hover-content.js +167 -129
  57. package/dist/hover.js +285 -245
  58. package/dist/index.js +10 -10
  59. package/dist/inlay-hints.js +369 -324
  60. package/dist/key-priority.js +34 -31
  61. package/dist/language-ids.js +45 -42
  62. package/dist/line-edits.js +74 -28
  63. package/dist/line-text.js +18 -3
  64. package/dist/locales/en.json +38 -38
  65. package/dist/locales/nl.json +41 -41
  66. package/dist/models.js +1 -1
  67. package/dist/navigation-history.js +40 -41
  68. package/dist/navigation.js +141 -123
  69. package/dist/panel-size.js +8 -8
  70. package/dist/patch-decorations.js +236 -160
  71. package/dist/patch-layout.js +331 -262
  72. package/dist/paths.js +3 -3
  73. package/dist/peek-model.js +86 -69
  74. package/dist/peek.js +254 -192
  75. package/dist/pick.js +164 -157
  76. package/dist/popup-focus.js +5 -5
  77. package/dist/popup-placement.js +36 -21
  78. package/dist/popups.js +13 -13
  79. package/dist/project-language.js +228 -211
  80. package/dist/project-problems.js +81 -69
  81. package/dist/proposal-model.js +163 -126
  82. package/dist/recent-choices.js +25 -25
  83. package/dist/refresher.js +44 -49
  84. package/dist/rename-model.js +113 -113
  85. package/dist/rename.js +321 -233
  86. package/dist/rendered-docs.d.ts +4 -2
  87. package/dist/rendered-docs.js +384 -318
  88. package/dist/rendering-context.js +5 -3
  89. package/dist/rendering.js +22 -4
  90. package/dist/review-group.js +42 -39
  91. package/dist/row-host.d.ts +4 -2
  92. package/dist/row-host.js +90 -78
  93. package/dist/selection-ranges.js +21 -17
  94. package/dist/semantic-delta.js +22 -19
  95. package/dist/semantic-model.js +86 -73
  96. package/dist/semantic-tokens.js +201 -162
  97. package/dist/shortcut-keys.js +16 -10
  98. package/dist/signature-model.js +34 -28
  99. package/dist/signature.js +177 -129
  100. package/dist/snippet-field-transforms.js +118 -106
  101. package/dist/snippet-indent.js +48 -43
  102. package/dist/snippet-range-order.js +11 -11
  103. package/dist/snippet-session.js +703 -667
  104. package/dist/snippet-transform-client.js +59 -59
  105. package/dist/snippet-transform-format.js +285 -263
  106. package/dist/snippet-transform-protocol.js +1 -1
  107. package/dist/snippet-transform-queue.js +163 -153
  108. package/dist/snippet-transform-worker.js +42 -35
  109. package/dist/snippet-variable-names.js +1 -36
  110. package/dist/snippet-variables.js +53 -63
  111. package/dist/snippet.js +477 -405
  112. package/dist/split-alignment.js +47 -35
  113. package/dist/split-scroll.js +42 -40
  114. package/dist/symbol-links.js +33 -25
  115. package/dist/symbol-picker-model.js +126 -116
  116. package/dist/symbol-picker.js +98 -59
  117. package/dist/symbols.js +117 -88
  118. package/dist/testing.js +248 -165
  119. package/dist/timers.js +35 -30
  120. package/dist/use-panel-body.js +49 -8
  121. package/dist/use-problem-counts.js +29 -15
  122. package/dist/view-state.js +54 -33
  123. package/dist/word-diff.js +18 -18
  124. package/dist/workspace-edit.js +300 -249
  125. package/package.json +6 -6
  126. package/src/CallHierarchyPanel.tsx +2 -2
  127. package/src/DiffView.tsx +3 -3
  128. package/src/FindReplace.tsx +2 -2
  129. package/src/LanguagePopups.tsx +2 -0
  130. package/src/PeekPanel.tsx +3 -3
  131. package/src/RenderedDocs.tsx +9 -10
  132. package/src/SymbolPicker.tsx +2 -2
  133. package/src/diff-overlays.tsx +6 -4
  134. package/src/locales/en.json +38 -38
  135. package/src/locales/nl.json +41 -41
  136. package/src/rendered-docs.ts +14 -3
  137. package/src/row-host.ts +14 -3
@@ -24,37 +24,49 @@ const LINES_BACK = 60;
24
24
  const TOAST_ID = 'language-completion';
25
25
  const MAX_COMPLETION_CHARACTERS = 2_000_000;
26
26
  const MAX_COMPLETION_RANGES = 1_000;
27
- class CompletionLimitError extends Error {
28
- }
27
+ class CompletionLimitError extends Error {}
29
28
  function mapCompletionEdits(item, changes, mapRange) {
30
- // Changes apply one after another, so each is checked against the range as the earlier ones left it.
31
- const overlapsChange = (range) => {
32
- let { start, end } = range;
33
- for (const change of changes) {
34
- if (comparePositions(start, change.range.end) < 0 && comparePositions(change.range.start, end) < 0) {
35
- return true;
36
- }
37
- start = shiftPosition(start, change);
38
- end = shiftPosition(end, change);
39
- }
40
- return false;
41
- };
42
- let valid = true;
43
- const map = (range) => {
44
- valid &&= !overlapsChange(range);
45
- return mapRange(range);
46
- };
47
- const edit = item.textEdit;
48
- const mapped = {
49
- ...item,
50
- ...(edit === undefined
51
- ? {}
52
- : { textEdit: 'range' in edit ? { ...edit, range: map(edit.range) } : { ...edit, insert: map(edit.insert), replace: map(edit.replace) } }),
53
- ...(item.additionalTextEdits === undefined
54
- ? {}
55
- : { additionalTextEdits: item.additionalTextEdits.map((edit) => ({ ...edit, range: map(edit.range) })) })
56
- };
57
- return valid ? mapped : null;
29
+ // Changes apply one after another, so each is checked against the range as the earlier ones left it.
30
+ const overlapsChange = range => {
31
+ let {
32
+ start,
33
+ end
34
+ } = range;
35
+ for (const change of changes) {
36
+ if (comparePositions(start, change.range.end) < 0 && comparePositions(change.range.start, end) < 0) {
37
+ return true;
38
+ }
39
+ start = shiftPosition(start, change);
40
+ end = shiftPosition(end, change);
41
+ }
42
+ return false;
43
+ };
44
+ let valid = true;
45
+ const map = range => {
46
+ valid &&= !overlapsChange(range);
47
+ return mapRange(range);
48
+ };
49
+ const edit = item.textEdit;
50
+ const mapped = {
51
+ ...item,
52
+ ...(edit === undefined ? {} : {
53
+ textEdit: 'range' in edit ? {
54
+ ...edit,
55
+ range: map(edit.range)
56
+ } : {
57
+ ...edit,
58
+ insert: map(edit.insert),
59
+ replace: map(edit.replace)
60
+ }
61
+ }),
62
+ ...(item.additionalTextEdits === undefined ? {} : {
63
+ additionalTextEdits: item.additionalTextEdits.map(edit => ({
64
+ ...edit,
65
+ range: map(edit.range)
66
+ }))
67
+ })
68
+ };
69
+ return valid ? mapped : null;
58
70
  }
59
71
  /*
60
72
  * The suggestions that open while a word is typed. A single typed identifier character or a trigger
@@ -62,642 +74,821 @@ function mapCompletionEdits(item, changes, mapRange) {
62
74
  * is already there, unless the server called it incomplete. Enter inserts, Tab replaces, Escape closes.
63
75
  */
64
76
  export class CompletionFeature {
65
- language;
66
- timers;
67
- pool = [];
68
- incomplete = false;
69
- ranked = [];
70
- active = 0;
71
- /* Whether the person moved in the list; until then the active row is the best match, whatever the typing does to the list. */
72
- touched = false;
73
- activeKey = null;
74
- /* Asked for with the keyboard, which is as good as moving in the list when a commit character is typed. */
75
- explicit = false;
76
- detailsOpen = true;
77
- start = null;
78
- server = '';
79
- request = 0;
80
- controller = null;
81
- resolving = null;
82
- accepting = null;
83
- /* 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. */
84
- rankedFor = null;
85
- requestTimer;
86
- resolveTimer;
87
- textRevision = 0;
88
- acceptSequence = 0;
89
- disposed = false;
90
- resolved = new Map();
91
- constructor(language, timers = realTimers) {
92
- this.language = language;
93
- this.timers = timers;
94
- const { editor } = language;
95
- const offs = [
96
- language.onKey((event) => this.key(event), KEY_PRIORITY.completion),
97
- editor.onTextChange((change) => this.edited(change)),
98
- editor.onCaret((position) => this.caretMoved(position)),
99
- editor.onBlur(() => {
100
- this.acceptSequence++;
101
- this.accepting?.abort();
102
- this.close();
103
- })
104
- ];
105
- language.onDispose(() => {
106
- this.disposed = true;
107
- this.accepting?.abort();
108
- for (const off of offs) {
109
- off();
110
- }
111
- this.close();
112
- });
77
+ language;
78
+ timers;
79
+ pool = [];
80
+ incomplete = false;
81
+ ranked = [];
82
+ active = 0;
83
+ /* Whether the person moved in the list; until then the active row is the best match, whatever the typing does to the list. */
84
+ touched = false;
85
+ activeKey = null;
86
+ /* Asked for with the keyboard, which is as good as moving in the list when a commit character is typed. */
87
+ explicit = false;
88
+ detailsOpen = true;
89
+ start = null;
90
+ server = '';
91
+ request = 0;
92
+ controller = null;
93
+ resolving = null;
94
+ accepting = null;
95
+ /* 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. */
96
+ rankedFor = null;
97
+ requestTimer;
98
+ resolveTimer;
99
+ textRevision = 0;
100
+ acceptSequence = 0;
101
+ disposed = false;
102
+ resolved = new Map();
103
+ constructor(language, timers = realTimers) {
104
+ this.language = language;
105
+ this.timers = timers;
106
+ const {
107
+ editor
108
+ } = language;
109
+ const offs = [language.onKey(event => this.key(event), KEY_PRIORITY.completion), editor.onTextChange(change => this.edited(change)), editor.onCaret(position => this.caretMoved(position)), editor.onBlur(() => {
110
+ this.acceptSequence++;
111
+ this.accepting?.abort();
112
+ this.close();
113
+ })];
114
+ language.onDispose(() => {
115
+ this.disposed = true;
116
+ this.accepting?.abort();
117
+ for (const off of offs) {
118
+ off();
119
+ }
120
+ this.close();
121
+ });
122
+ }
123
+ get isOpen() {
124
+ return this.language.popups.getState().completion !== null;
125
+ }
126
+ /* Asks for suggestions at the caret now, as Ctrl+Space does. */
127
+ invoke() {
128
+ this.language.pick.close();
129
+ this.explicit = true;
130
+ this.beginRequest({
131
+ triggerKind: 1
132
+ }, 0);
133
+ }
134
+ move(delta) {
135
+ if (this.ranked.length === 0) {
136
+ return;
113
137
  }
114
- get isOpen() {
115
- return this.language.popups.getState().completion !== null;
138
+ const last = this.ranked.length - 1;
139
+ // One step past either end goes round, a page does not.
140
+ this.active = Math.abs(delta) === 1 ? (this.active + delta + last + 1) % (last + 1) : Math.max(0, Math.min(last, this.active + delta));
141
+ this.touched = true;
142
+ this.publish();
143
+ this.scheduleResolve();
144
+ }
145
+ /* Picks a row by a click or a key. It never rejects: a failure is told to the person, so a caller can leave the promise alone. */
146
+ async accept(replace, index = this.active, commit) {
147
+ try {
148
+ await this.insert(replace, index, commit);
149
+ } catch (error) {
150
+ const {
151
+ project
152
+ } = this.language;
153
+ project.host.notify?.({
154
+ id: TOAST_ID,
155
+ kind: 'error',
156
+ title: project.i18n.t('editor:language.completion.failed', {
157
+ message: messageOf(error)
158
+ })
159
+ });
116
160
  }
117
- /* Asks for suggestions at the caret now, as Ctrl+Space does. */
118
- invoke() {
119
- this.language.pick.close();
120
- this.explicit = true;
121
- this.beginRequest({ triggerKind: 1 }, 0);
161
+ }
162
+ async insert(replace, index, commit) {
163
+ const entry = this.ranked[index];
164
+ if (entry === undefined) {
165
+ return;
122
166
  }
123
- move(delta) {
124
- if (this.ranked.length === 0) {
125
- return;
126
- }
127
- const last = this.ranked.length - 1;
128
- // One step past either end goes round, a page does not.
129
- this.active = Math.abs(delta) === 1 ? (this.active + delta + last + 1) % (last + 1) : Math.max(0, Math.min(last, this.active + delta));
130
- this.touched = true;
131
- this.publish();
132
- this.scheduleResolve();
167
+ const {
168
+ editor,
169
+ project,
170
+ uri,
171
+ snippets
172
+ } = this.language;
173
+ const acceptSequence = ++this.acceptSequence;
174
+ this.accepting?.abort();
175
+ const accepting = new AbortController();
176
+ this.accepting = accepting;
177
+ const snippetToken = snippets.token;
178
+ const inSnippet = snippets.isActive;
179
+ const exitChanges = [];
180
+ if (snippets.hasPendingTransforms) {
181
+ this.close();
182
+ if (!(await snippets.settleTransforms(() => !this.disposed && acceptSequence === this.acceptSequence && snippets.token === snippetToken, (changes, map) => exitChanges.push({
183
+ changes,
184
+ map
185
+ })))) {
186
+ return;
187
+ }
133
188
  }
134
- /* Picks a row by a click or a key. It never rejects: a failure is told to the person, so a caller can leave the promise alone. */
135
- async accept(replace, index = this.active, commit) {
136
- try {
137
- await this.insert(replace, index, commit);
138
- }
139
- catch (error) {
140
- const { project } = this.language;
141
- project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.failed', { message: messageOf(error) }) });
142
- }
189
+ const acceptedSelections = editor.getSelections();
190
+ const revision = this.textRevision;
191
+ const current = () => {
192
+ if (this.disposed || acceptSequence !== this.acceptSequence || revision !== this.textRevision || inSnippet && snippets.token !== snippetToken) {
193
+ return false;
194
+ }
195
+ const selections = editor.getSelections();
196
+ return selections.length === acceptedSelections.length && selections.every((range, index) => comparePositions(range.start, acceptedSelections[index].start) === 0 && comparePositions(range.end, acceptedSelections[index].end) === 0);
197
+ };
198
+ recentChoicesOf(this.language.languageId).record(entry.item);
199
+ let item = this.resolved.get(entry.item) ?? entry.item;
200
+ const needsResolve = !this.resolved.has(entry.item) && project.service.providerOptions('completionItem/resolve', uri) !== undefined;
201
+ this.close();
202
+ if (needsResolve) {
203
+ // Resolving can add the import an item needs; an item that cannot be resolved is still inserted as it is.
204
+ item = await this.resolveNow(entry.item, accepting.signal);
143
205
  }
144
- async insert(replace, index, commit) {
145
- const entry = this.ranked[index];
146
- if (entry === undefined) {
147
- return;
148
- }
149
- const { editor, project, uri, snippets } = this.language;
150
- const acceptSequence = ++this.acceptSequence;
151
- this.accepting?.abort();
152
- const accepting = new AbortController();
153
- this.accepting = accepting;
154
- const snippetToken = snippets.token;
155
- const inSnippet = snippets.isActive;
156
- const exitChanges = [];
157
- if (snippets.hasPendingTransforms) {
158
- this.close();
159
- if (!(await snippets.settleTransforms(() => !this.disposed && acceptSequence === this.acceptSequence && snippets.token === snippetToken, (changes, map) => exitChanges.push({ changes, map })))) {
160
- return;
161
- }
162
- }
163
- const acceptedSelections = editor.getSelections();
164
- const revision = this.textRevision;
165
- const current = () => {
166
- if (this.disposed || acceptSequence !== this.acceptSequence || revision !== this.textRevision || (inSnippet && snippets.token !== snippetToken)) {
167
- return false;
168
- }
169
- const selections = editor.getSelections();
170
- return (selections.length === acceptedSelections.length &&
171
- selections.every((range, index) => comparePositions(range.start, acceptedSelections[index].start) === 0 &&
172
- comparePositions(range.end, acceptedSelections[index].end) === 0));
206
+ if (!current()) {
207
+ return;
208
+ }
209
+ for (const {
210
+ changes,
211
+ map
212
+ } of exitChanges) {
213
+ const mapped = mapCompletionEdits(item, changes, map);
214
+ if (mapped === null) {
215
+ return;
216
+ }
217
+ item = mapped;
218
+ }
219
+ const caret = editor.getCaret();
220
+ const lineBefore = textBeforeOf(editor, caret);
221
+ const lineAfter = editor.textInRange({
222
+ start: caret,
223
+ end: {
224
+ line: caret.line,
225
+ character: Number.MAX_SAFE_INTEGER
226
+ }
227
+ });
228
+ const carets = editor.getSelections();
229
+ const source = item.textEdit?.newText ?? item.insertText ?? item.label;
230
+ if (source.length * carets.length > MAX_COMPLETION_CHARACTERS) {
231
+ this.refuseLargeCompletion();
232
+ return;
233
+ }
234
+ let characters = 0;
235
+ let ranges = 0;
236
+ const now = new Date();
237
+ const template = item.insertTextFormat === 2 ? compileSnippet(item.textEdit?.newText ?? item.insertText ?? item.label) : undefined;
238
+ const finishes = new Map();
239
+ const renderAt = (position, selection) => {
240
+ const line = editor.textInRange({
241
+ start: {
242
+ line: position.line,
243
+ character: 0
244
+ },
245
+ end: {
246
+ line: position.line,
247
+ character: Number.MAX_SAFE_INTEGER
248
+ }
249
+ });
250
+ const word = wordRangeAt(line, position);
251
+ const resolver = createSnippetResolver({
252
+ uri,
253
+ languageId: this.language.languageId,
254
+ position,
255
+ selection,
256
+ selectedText: editor.textInRange(selection),
257
+ currentLine: line,
258
+ currentWord: word === null ? '' : editor.textInRange(word),
259
+ cursorIndex: Math.max(0, carets.findIndex(range => comparePositions(range.start, selection.start) === 0 && comparePositions(range.end, selection.end) === 0)),
260
+ cursorCount: carets.length,
261
+ folder: project.host.folder,
262
+ path: fileUriToPath(uri) ?? project.host.pathOfUri?.(uri) ?? undefined
263
+ }, project.host.resolveSnippetVariable?.bind(project.host), {
264
+ now
265
+ });
266
+ const atItem = position === caret || item.textEdit === undefined ? item : {
267
+ ...item,
268
+ textEdit: undefined,
269
+ insertText: item.textEdit?.newText ?? item.insertText
270
+ };
271
+ const editStart = editRangeOf(atItem, replace)?.start;
272
+ // A commit character follows the name, so accepting must not also add a call.
273
+ const finish = variableTransforms => {
274
+ const insertion = insertionOf(atItem, position, line.slice(0, position.character), replace, commit === undefined ? line.slice(position.character) : '(', {
275
+ template,
276
+ variableTransforms,
277
+ resolve: resolver,
278
+ indentationBefore: editStart === undefined ? undefined : editor.textInRange({
279
+ start: {
280
+ line: editStart.line,
281
+ character: 0
282
+ },
283
+ end: editStart
284
+ })
285
+ });
286
+ return commit === undefined ? insertion : {
287
+ ...insertion,
288
+ text: insertion.text + commit
173
289
  };
174
- recentChoicesOf(this.language.languageId).record(entry.item);
175
- let item = this.resolved.get(entry.item) ?? entry.item;
176
- const needsResolve = !this.resolved.has(entry.item) && project.service.providerOptions('completionItem/resolve', uri) !== undefined;
177
- this.close();
178
- if (needsResolve) {
179
- // Resolving can add the import an item needs; an item that cannot be resolved is still inserted as it is.
180
- item = await this.resolveNow(entry.item, accepting.signal);
181
- }
182
- if (!current()) {
183
- return;
184
- }
185
- for (const { changes, map } of exitChanges) {
186
- const mapped = mapCompletionEdits(item, changes, map);
187
- if (mapped === null) {
188
- return;
189
- }
190
- item = mapped;
191
- }
192
- const caret = editor.getCaret();
193
- const lineBefore = textBeforeOf(editor, caret);
194
- const lineAfter = editor.textInRange({ start: caret, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } });
195
- const carets = editor.getSelections();
196
- const source = item.textEdit?.newText ?? item.insertText ?? item.label;
197
- if (source.length * carets.length > MAX_COMPLETION_CHARACTERS) {
198
- this.refuseLargeCompletion();
199
- return;
200
- }
201
- let characters = 0;
202
- let ranges = 0;
203
- const now = new Date();
204
- const template = item.insertTextFormat === 2 ? compileSnippet(item.textEdit?.newText ?? item.insertText ?? item.label) : undefined;
205
- const finishes = new Map();
206
- const renderAt = (position, selection) => {
207
- const line = editor.textInRange({ start: { line: position.line, character: 0 }, end: { line: position.line, character: Number.MAX_SAFE_INTEGER } });
208
- const word = wordRangeAt(line, position);
209
- const resolver = createSnippetResolver({
210
- uri,
211
- languageId: this.language.languageId,
212
- position,
213
- selection,
214
- selectedText: editor.textInRange(selection),
215
- currentLine: line,
216
- currentWord: word === null ? '' : editor.textInRange(word),
217
- cursorIndex: Math.max(0, carets.findIndex((range) => comparePositions(range.start, selection.start) === 0 && comparePositions(range.end, selection.end) === 0)),
218
- cursorCount: carets.length,
219
- folder: project.host.folder,
220
- path: fileUriToPath(uri) ?? project.host.pathOfUri?.(uri) ?? undefined
221
- }, project.host.resolveSnippetVariable?.bind(project.host), { now });
222
- const atItem = position === caret || item.textEdit === undefined
223
- ? item
224
- : { ...item, textEdit: undefined, insertText: item.textEdit?.newText ?? item.insertText };
225
- const editStart = editRangeOf(atItem, replace)?.start;
226
- // A commit character follows the name, so accepting must not also add a call.
227
- const finish = (variableTransforms) => {
228
- const insertion = insertionOf(atItem, position, line.slice(0, position.character), replace, commit === undefined ? line.slice(position.character) : '(', {
229
- template,
230
- variableTransforms,
231
- resolve: resolver,
232
- indentationBefore: editStart === undefined ? undefined : editor.textInRange({ start: { line: editStart.line, character: 0 }, end: editStart })
233
- });
234
- return commit === undefined ? insertion : { ...insertion, text: insertion.text + commit };
235
- };
236
- const rendered = finish();
237
- finishes.set(rendered, finish);
238
- characters += rendered.text.length;
239
- ranges +=
240
- (rendered.variableTransforms?.length ?? 0) +
241
- (rendered.fields?.reduce((count, field) => count + field.ranges.length + (field.linked?.length ?? 0), 1) ?? rendered.stops.length);
242
- if (rendered.limited || characters > MAX_COMPLETION_CHARACTERS || ranges > MAX_COMPLETION_RANGES) {
243
- throw new CompletionLimitError();
244
- }
245
- return rendered;
290
+ };
291
+ const rendered = finish();
292
+ finishes.set(rendered, finish);
293
+ characters += rendered.text.length;
294
+ ranges += (rendered.variableTransforms?.length ?? 0) + (rendered.fields?.reduce((count, field) => count + field.ranges.length + (field.linked?.length ?? 0), 1) ?? rendered.stops.length);
295
+ if (rendered.limited || characters > MAX_COMPLETION_CHARACTERS || ranges > MAX_COMPLETION_RANGES) {
296
+ throw new CompletionLimitError();
297
+ }
298
+ return rendered;
299
+ };
300
+ try {
301
+ const planned = renderAt(caret, editor.getSelection());
302
+ const {
303
+ before,
304
+ rest
305
+ } = this.surroundings(planned, caret, lineAfter);
306
+ const plan = commit === undefined ? planCall({
307
+ item,
308
+ languageId: this.language.languageId,
309
+ insertion: planned,
310
+ before,
311
+ rest,
312
+ replace
313
+ }) : {
314
+ action: 'none'
315
+ };
316
+ let main = plan.action === 'add' ? withParentheses(planned, plan.inside) : planned;
317
+ const extras = (item.additionalTextEdits ?? []).map(edit => ({
318
+ range: edit.range,
319
+ text: edit.newText
320
+ }));
321
+ const instances = new Map();
322
+ const continuing = inSnippet && snippets.isActive && snippetToken === snippets.token;
323
+ const mirrored = mirroredInsertions(carets, editor.getSelection(), caret, range => editor.textInRange(range), main, continuing).map(insertion => {
324
+ const selection = carets.find(range => range.start.line === insertion.range.start.line && range.start.character >= insertion.range.start.character && range.start.character <= insertion.range.end.character);
325
+ const position = selection[continuing ? 'end' : 'start'];
326
+ const own = renderAt(position, selection);
327
+ const rendered = plan.action === 'add' ? withParentheses(own, plan.inside) : own;
328
+ const edit = {
329
+ ...insertion,
330
+ text: rendered.text
246
331
  };
247
- try {
248
- const planned = renderAt(caret, editor.getSelection());
249
- const { before, rest } = this.surroundings(planned, caret, lineAfter);
250
- const plan = commit === undefined ? planCall({ item, languageId: this.language.languageId, insertion: planned, before, rest, replace }) : { action: 'none' };
251
- let main = plan.action === 'add' ? withParentheses(planned, plan.inside) : planned;
252
- const extras = (item.additionalTextEdits ?? []).map((edit) => ({ range: edit.range, text: edit.newText }));
253
- const instances = new Map();
254
- const continuing = inSnippet && snippets.isActive && snippetToken === snippets.token;
255
- const mirrored = mirroredInsertions(carets, editor.getSelection(), caret, (range) => editor.textInRange(range), main, continuing).map((insertion) => {
256
- const selection = carets.find((range) => range.start.line === insertion.range.start.line &&
257
- range.start.character >= insertion.range.start.character &&
258
- range.start.character <= insertion.range.end.character);
259
- const position = selection[continuing ? 'end' : 'start'];
260
- const own = renderAt(position, selection);
261
- const rendered = plan.action === 'add' ? withParentheses(own, plan.inside) : own;
262
- const edit = { ...insertion, text: rendered.text };
263
- const instance = { ...rendered, range: insertion.range };
264
- instances.set(edit, instance);
265
- const finish = finishes.get(own);
266
- if (finish !== undefined) {
267
- finishes.set(instance, (values) => ({ ...finish(values), range: insertion.range }));
268
- }
269
- return edit;
270
- });
271
- const drafts = [...mirrored.map((edit) => instances.get(edit)), main];
272
- const jobs = [];
273
- const keys = new Map();
274
- const slots = drafts.map((draft) => (draft.variableTransforms ?? []).map((job) => {
275
- const key = JSON.stringify(job);
276
- let slot = keys.get(key);
277
- if (slot === undefined) {
278
- slot = jobs.length;
279
- keys.set(key, slot);
280
- jobs.push(job);
281
- }
282
- return slot;
283
- }));
284
- if (jobs.length > MAX_TRANSFORM_JOBS || jobs.reduce((sum, job) => sum + job.value.length, 0) > MAX_TRANSFORM_WORK) {
285
- throw new CompletionLimitError();
286
- }
287
- if (jobs.length > 0 && project.host.snippetTransformWorker !== undefined) {
288
- const values = await snippets.transformVariables(jobs, current);
289
- if (!current()) {
290
- return;
291
- }
292
- if (values !== null) {
293
- const transformed = drafts.map((draft, index) => slots[index].length === 0 ? draft : finishes.get(draft)(slots[index].map((slot) => values[slot])));
294
- if (transformed.some((draft) => draft.limited) ||
295
- transformed.reduce((sum, draft) => sum + draft.text.length, 0) > MAX_COMPLETION_CHARACTERS) {
296
- throw new CompletionLimitError();
297
- }
298
- main = transformed.at(-1);
299
- mirrored.forEach((edit, index) => {
300
- edit.text = transformed[index].text;
301
- instances.set(edit, transformed[index]);
302
- });
303
- }
304
- }
305
- const primary = { range: main.range, text: main.text };
306
- const changes = [primary, ...mirrored, ...extras];
307
- if (changes.reduce((total, edit) => total + edit.text.length, 0) > MAX_COMPLETION_CHARACTERS) {
308
- throw new CompletionLimitError();
309
- }
310
- // Imports and earlier carets move each insertion independently; the primary instance stays last.
311
- const starts = [...mirrored, primary].map((insertion) => changes
312
- .filter((change) => change !== insertion && comparePositions(change.range.end, insertion.range.start) <= 0)
313
- .sort((left, right) => comparePositions(right.range.start, left.range.start))
314
- .reduce((place, change) => shiftPosition(place, change), insertion.range.start));
315
- const applied = inSnippet ? snippets.applyEdits(changes, snippetToken, extras) : editor.applyEdits(changes);
316
- if (!applied) {
317
- return;
318
- }
319
- const landed = editor.getSelections();
320
- if (commit === undefined) {
321
- this.language.snippets.beginInstances([...mirrored, primary].map((insertion, index) => {
322
- const rendered = insertion === primary ? main : instances.get(insertion);
323
- return { start: starts[index], text: rendered.text, stops: rendered.stops, fields: rendered.fields };
324
- }), { nested: continuing && snippets.isActive });
325
- if (plan.action === 'add' && plan.inside && mirrored.length > 0 && main.fields === undefined) {
326
- this.placeMirroredCarets(carets, mirrored, landed);
327
- }
328
- if (plan.action === 'enter') {
329
- const end = editor.getCaret();
330
- editor.setCaret({ line: end.line, character: end.character + plan.shift });
331
- }
332
- }
333
- else {
334
- if (this.triggerCharacters().includes(commit)) {
335
- this.beginRequest({ triggerKind: 2, triggerCharacter: commit }, 0);
336
- }
337
- }
338
- this.afterInsert(item, commit, lineBefore);
339
- editor.focus();
340
- }
341
- catch (error) {
342
- if (!current()) {
343
- return;
344
- }
345
- if (error instanceof SnippetTransformQueueError) {
346
- project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformBusy') });
347
- }
348
- else if (error instanceof CompletionLimitError || (error instanceof SnippetTransformError && error.kind === 'range')) {
349
- this.refuseLargeCompletion();
350
- }
351
- else if (error instanceof SnippetTransformError) {
352
- project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformFailed') });
353
- }
354
- else {
355
- throw error;
356
- }
357
- }
358
- }
359
- refuseLargeCompletion() {
360
- this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.completion.tooLarge') });
361
- }
362
- /* The carets that got the same `()` go inside it like the primary one, which the snippet already placed. */
363
- placeMirroredCarets(before, mirrored, landed) {
364
- const { editor } = this.language;
365
- const primary = before.length - 1;
366
- const selections = landed.map((range, index) => {
367
- const caret = before[index].start;
368
- const got = mirrored.some((edit) => edit.range.start.line === caret.line && edit.range.start.character <= caret.character && caret.character <= edit.range.end.character);
369
- if (index === primary || !got) {
370
- return range;
371
- }
372
- const stop = { line: range.end.line, character: range.end.character - 1 };
373
- return { start: stop, end: stop };
332
+ const instance = {
333
+ ...rendered,
334
+ range: insertion.range
335
+ };
336
+ instances.set(edit, instance);
337
+ const finish = finishes.get(own);
338
+ if (finish !== undefined) {
339
+ finishes.set(instance, values => ({
340
+ ...finish(values),
341
+ range: insertion.range
342
+ }));
343
+ }
344
+ return edit;
345
+ });
346
+ const drafts = [...mirrored.map(edit => instances.get(edit)), main];
347
+ const jobs = [];
348
+ const keys = new Map();
349
+ const slots = drafts.map(draft => (draft.variableTransforms ?? []).map(job => {
350
+ const key = JSON.stringify(job);
351
+ let slot = keys.get(key);
352
+ if (slot === undefined) {
353
+ slot = jobs.length;
354
+ keys.set(key, slot);
355
+ jobs.push(job);
356
+ }
357
+ return slot;
358
+ }));
359
+ if (jobs.length > MAX_TRANSFORM_JOBS || jobs.reduce((sum, job) => sum + job.value.length, 0) > MAX_TRANSFORM_WORK) {
360
+ throw new CompletionLimitError();
361
+ }
362
+ if (jobs.length > 0 && project.host.snippetTransformWorker !== undefined) {
363
+ const values = await snippets.transformVariables(jobs, current);
364
+ if (!current()) {
365
+ return;
366
+ }
367
+ if (values !== null) {
368
+ const transformed = drafts.map((draft, index) => slots[index].length === 0 ? draft : finishes.get(draft)(slots[index].map(slot => values[slot])));
369
+ if (transformed.some(draft => draft.limited) || transformed.reduce((sum, draft) => sum + draft.text.length, 0) > MAX_COMPLETION_CHARACTERS) {
370
+ throw new CompletionLimitError();
371
+ }
372
+ main = transformed.at(-1);
373
+ mirrored.forEach((edit, index) => {
374
+ edit.text = transformed[index].text;
375
+ instances.set(edit, transformed[index]);
376
+ });
377
+ }
378
+ }
379
+ const primary = {
380
+ range: main.range,
381
+ text: main.text
382
+ };
383
+ const changes = [primary, ...mirrored, ...extras];
384
+ if (changes.reduce((total, edit) => total + edit.text.length, 0) > MAX_COMPLETION_CHARACTERS) {
385
+ throw new CompletionLimitError();
386
+ }
387
+ // Imports and earlier carets move each insertion independently; the primary instance stays last.
388
+ const starts = [...mirrored, primary].map(insertion => changes.filter(change => change !== insertion && comparePositions(change.range.end, insertion.range.start) <= 0).sort((left, right) => comparePositions(right.range.start, left.range.start)).reduce((place, change) => shiftPosition(place, change), insertion.range.start));
389
+ const applied = inSnippet ? snippets.applyEdits(changes, snippetToken, extras) : editor.applyEdits(changes);
390
+ if (!applied) {
391
+ return;
392
+ }
393
+ const landed = editor.getSelections();
394
+ if (commit === undefined) {
395
+ this.language.snippets.beginInstances([...mirrored, primary].map((insertion, index) => {
396
+ const rendered = insertion === primary ? main : instances.get(insertion);
397
+ return {
398
+ start: starts[index],
399
+ text: rendered.text,
400
+ stops: rendered.stops,
401
+ fields: rendered.fields
402
+ };
403
+ }), {
404
+ nested: continuing && snippets.isActive
374
405
  });
375
- selections[primary] = editor.getSelection();
376
- editor.setSelections(selections);
377
- }
378
- /* The lines above the insertion down to where it starts, and the rest of the line past what it replaces. */
379
- surroundings(insertion, caret, lineAfter) {
380
- const { editor } = this.language;
381
- const { start, end } = insertion.range;
382
- const before = editor.textInRange({ start: { line: Math.max(0, start.line - LINES_BACK), character: 0 }, end: start });
383
- const rest = end.line === caret.line ? lineAfter.slice(Math.max(0, end.character - caret.character)) : '';
384
- return { before, rest };
385
- }
386
- /* Parameter info for a call that was opened (or that the server asks for), then the item's own command. */
387
- afterInsert(item, commit, lineBefore) {
388
- const { editor, project, uri, signature } = this.language;
389
- const { start } = editor.getSelection();
390
- const insideCall = start.character > 0 && editor.textInRange({ start: { line: start.line, character: start.character - 1 }, end: start }) === '(';
391
- if (commit === '(' || (insideCall && isCallItem(item, lineBefore))) {
392
- signature.trigger();
393
- }
394
- const { command } = item;
395
- if (command === undefined) {
396
- return;
397
- }
398
- if (command.command === PARAMETER_HINTS_COMMAND) {
399
- signature.trigger();
400
- }
401
- else {
402
- project.service.executeCommand(uri, command).catch(() => undefined);
403
- }
406
+ if (plan.action === 'add' && plan.inside && mirrored.length > 0 && main.fields === undefined) {
407
+ this.placeMirroredCarets(carets, mirrored, landed);
408
+ }
409
+ if (plan.action === 'enter') {
410
+ const end = editor.getCaret();
411
+ editor.setCaret({
412
+ line: end.line,
413
+ character: end.character + plan.shift
414
+ });
415
+ }
416
+ } else {
417
+ if (this.triggerCharacters().includes(commit)) {
418
+ this.beginRequest({
419
+ triggerKind: 2,
420
+ triggerCharacter: commit
421
+ }, 0);
422
+ }
423
+ }
424
+ this.afterInsert(item, commit, lineBefore);
425
+ editor.focus();
426
+ } catch (error) {
427
+ if (!current()) {
428
+ return;
429
+ }
430
+ if (error instanceof SnippetTransformQueueError) {
431
+ project.host.notify?.({
432
+ id: TOAST_ID,
433
+ kind: 'error',
434
+ title: project.i18n.t('editor:language.completion.transformBusy')
435
+ });
436
+ } else if (error instanceof CompletionLimitError || error instanceof SnippetTransformError && error.kind === 'range') {
437
+ this.refuseLargeCompletion();
438
+ } else if (error instanceof SnippetTransformError) {
439
+ project.host.notify?.({
440
+ id: TOAST_ID,
441
+ kind: 'error',
442
+ title: project.i18n.t('editor:language.completion.transformFailed')
443
+ });
444
+ } else {
445
+ throw error;
446
+ }
404
447
  }
405
- /*
406
- * Resolving gives the item its documentation and the import it needs, but only for the text it was
407
- * made for. When the person typed on since, the list is asked for again at the new text and the same
408
- * suggestion is resolved in that one; an item that cannot be resolved is still inserted as it is.
409
- */
410
- async resolveNow(item, signal) {
411
- const { editor, project, uri } = this.language;
412
- try {
413
- return { ...item, ...(await project.service.resolveCompletion(uri, item, { signal })) };
414
- }
415
- catch (error) {
416
- if (!(error instanceof StaleResultError)) {
417
- return item;
418
- }
419
- }
420
- try {
421
- const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 }, { signal })).items.find((candidate) => candidate.label === item.label && candidate.kind === item.kind);
422
- return fresh === undefined ? item : { ...fresh, ...(await project.service.resolveCompletion(uri, fresh, { signal }).catch(() => fresh)) };
423
- }
424
- catch {
425
- return item;
426
- }
448
+ }
449
+ refuseLargeCompletion() {
450
+ this.language.project.host.notify?.({
451
+ id: TOAST_ID,
452
+ kind: 'error',
453
+ title: this.language.project.i18n.t('editor:language.completion.tooLarge')
454
+ });
455
+ }
456
+ /* The carets that got the same `()` go inside it like the primary one, which the snippet already placed. */
457
+ placeMirroredCarets(before, mirrored, landed) {
458
+ const {
459
+ editor
460
+ } = this.language;
461
+ const primary = before.length - 1;
462
+ const selections = landed.map((range, index) => {
463
+ const caret = before[index].start;
464
+ const got = mirrored.some(edit => edit.range.start.line === caret.line && edit.range.start.character <= caret.character && caret.character <= edit.range.end.character);
465
+ if (index === primary || !got) {
466
+ return range;
467
+ }
468
+ const stop = {
469
+ line: range.end.line,
470
+ character: range.end.character - 1
471
+ };
472
+ return {
473
+ start: stop,
474
+ end: stop
475
+ };
476
+ });
477
+ selections[primary] = editor.getSelection();
478
+ editor.setSelections(selections);
479
+ }
480
+ /* The lines above the insertion down to where it starts, and the rest of the line past what it replaces. */
481
+ surroundings(insertion, caret, lineAfter) {
482
+ const {
483
+ editor
484
+ } = this.language;
485
+ const {
486
+ start,
487
+ end
488
+ } = insertion.range;
489
+ const before = editor.textInRange({
490
+ start: {
491
+ line: Math.max(0, start.line - LINES_BACK),
492
+ character: 0
493
+ },
494
+ end: start
495
+ });
496
+ const rest = end.line === caret.line ? lineAfter.slice(Math.max(0, end.character - caret.character)) : '';
497
+ return {
498
+ before,
499
+ rest
500
+ };
501
+ }
502
+ /* Parameter info for a call that was opened (or that the server asks for), then the item's own command. */
503
+ afterInsert(item, commit, lineBefore) {
504
+ const {
505
+ editor,
506
+ project,
507
+ uri,
508
+ signature
509
+ } = this.language;
510
+ const {
511
+ start
512
+ } = editor.getSelection();
513
+ const insideCall = start.character > 0 && editor.textInRange({
514
+ start: {
515
+ line: start.line,
516
+ character: start.character - 1
517
+ },
518
+ end: start
519
+ }) === '(';
520
+ if (commit === '(' || insideCall && isCallItem(item, lineBefore)) {
521
+ signature.trigger();
427
522
  }
428
- close() {
429
- this.request++;
430
- this.controller?.abort();
431
- this.resolving?.abort();
432
- this.timers.clear(this.requestTimer);
433
- this.timers.clear(this.resolveTimer);
434
- this.pool = [];
435
- this.ranked = [];
436
- this.rankedFor = null;
437
- this.touched = false;
438
- this.explicit = false;
439
- this.activeKey = null;
440
- this.start = null;
441
- this.resolved.clear();
442
- if (this.language.popups.getState().completion !== null) {
443
- this.language.popups.setState({ completion: null });
444
- }
523
+ const {
524
+ command
525
+ } = item;
526
+ if (command === undefined) {
527
+ return;
445
528
  }
446
- key(event) {
447
- if (isShortcut(this.language.shortcuts.triggerCompletion, event)) {
448
- if (this.isOpen) {
449
- this.detailsOpen = !this.detailsOpen;
450
- this.publish();
451
- }
452
- else {
453
- this.invoke();
454
- }
455
- return true;
456
- }
457
- if (!this.isOpen || event.ctrlKey || event.metaKey || event.altKey) {
458
- return false;
459
- }
460
- if (this.commits(event)) {
461
- void this.accept(false, this.active, event.key);
462
- return true;
463
- }
464
- switch (event.key) {
465
- case 'ArrowDown':
466
- this.move(1);
467
- return true;
468
- case 'ArrowUp':
469
- this.move(-1);
470
- return true;
471
- case 'PageDown':
472
- this.move(PAGE);
473
- return true;
474
- case 'PageUp':
475
- this.move(-PAGE);
476
- return true;
477
- case 'Enter':
478
- case 'Tab':
479
- if (event.shiftKey) {
480
- return false;
481
- }
482
- void this.accept(event.key === 'Tab');
483
- return true;
484
- case 'Escape':
485
- this.close();
486
- return true;
487
- default:
488
- return false;
489
- }
529
+ if (command.command === PARAMETER_HINTS_COMMAND) {
530
+ signature.trigger();
531
+ } else {
532
+ project.service.executeCommand(uri, command).catch(() => undefined);
490
533
  }
491
- /* A character the active item commits on, typed once the person has moved in the list or asked for it, which is when Enter would not be the way they chose. */
492
- commits(event) {
493
- if ((!this.touched && !this.explicit) || event.key.length !== 1 || event.isComposing) {
494
- return false;
495
- }
496
- return this.ranked[this.active]?.item.commitCharacters?.includes(event.key) === true;
534
+ }
535
+ /*
536
+ * Resolving gives the item its documentation and the import it needs, but only for the text it was
537
+ * made for. When the person typed on since, the list is asked for again at the new text and the same
538
+ * suggestion is resolved in that one; an item that cannot be resolved is still inserted as it is.
539
+ */
540
+ async resolveNow(item, signal) {
541
+ const {
542
+ editor,
543
+ project,
544
+ uri
545
+ } = this.language;
546
+ try {
547
+ return {
548
+ ...item,
549
+ ...(await project.service.resolveCompletion(uri, item, {
550
+ signal
551
+ }))
552
+ };
553
+ } catch (error) {
554
+ if (!(error instanceof StaleResultError)) {
555
+ return item;
556
+ }
497
557
  }
498
- edited(change) {
499
- this.textRevision++;
500
- if (change.source !== 'input' || change.changes.length !== 1) {
501
- this.close();
502
- return;
503
- }
504
- const { range, text } = change.changes[0];
505
- const triggers = this.triggerCharacters();
506
- const last = Array.from(text).at(-1) ?? '';
507
- const typedIdentifier = text !== '' && Array.from(text).every(isIdentifierCharacter);
508
- if (text.length === 1 && triggers.includes(last)) {
509
- this.close();
510
- this.beginRequest({ triggerKind: 2, triggerCharacter: last }, 0);
511
- }
512
- else if (isEmptyRange(range) && opensCompletion(textBeforeOf(this.language.editor, range.start), text)) {
513
- this.close();
514
- this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
515
- }
516
- else if (this.isOpen) {
517
- if (text === '' || typedIdentifier) {
518
- this.refilter();
519
- }
520
- else {
521
- this.close();
522
- }
523
- }
524
- else if (text.length === 1 && typedIdentifier && isEmptyRange(range)) {
525
- this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
526
- }
527
- else if (text !== '') {
528
- // A request the last letter started, still waiting or out, would open the list after a character that ends the word.
529
- this.close();
530
- }
558
+ try {
559
+ const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), {
560
+ triggerKind: 1
561
+ }, {
562
+ signal
563
+ })).items.find(candidate => candidate.label === item.label && candidate.kind === item.kind);
564
+ return fresh === undefined ? item : {
565
+ ...fresh,
566
+ ...(await project.service.resolveCompletion(uri, fresh, {
567
+ signal
568
+ }).catch(() => fresh))
569
+ };
570
+ } catch {
571
+ return item;
531
572
  }
532
- caretMoved(position) {
533
- if (this.start === null) {
534
- return;
535
- }
536
- if (position.line !== this.start.line || position.character < this.start.character) {
537
- this.close();
538
- }
539
- else {
540
- this.refilter();
541
- }
573
+ }
574
+ close() {
575
+ this.request++;
576
+ this.controller?.abort();
577
+ this.resolving?.abort();
578
+ this.timers.clear(this.requestTimer);
579
+ this.timers.clear(this.resolveTimer);
580
+ this.pool = [];
581
+ this.ranked = [];
582
+ this.rankedFor = null;
583
+ this.touched = false;
584
+ this.explicit = false;
585
+ this.activeKey = null;
586
+ this.start = null;
587
+ this.resolved.clear();
588
+ if (this.language.popups.getState().completion !== null) {
589
+ this.language.popups.setState({
590
+ completion: null
591
+ });
542
592
  }
543
- triggerCharacters() {
544
- const { project, uri } = this.language;
545
- return project.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
593
+ }
594
+ key(event) {
595
+ if (isShortcut(this.language.shortcuts.triggerCompletion, event)) {
596
+ if (this.isOpen) {
597
+ this.detailsOpen = !this.detailsOpen;
598
+ this.publish();
599
+ } else {
600
+ this.invoke();
601
+ }
602
+ return true;
546
603
  }
547
- beginRequest(context, delay) {
548
- const { project, uri } = this.language;
549
- if (!project.service.supports(METHOD, uri)) {
550
- return;
551
- }
552
- this.timers.clear(this.requestTimer);
553
- const request = ++this.request;
554
- this.requestTimer = this.timers.set(() => void this.ask(context, request), delay);
555
- }
556
- async ask(context, request, refresh = false) {
557
- const { editor, project, uri } = this.language;
558
- const position = editor.getCaret();
559
- this.controller?.abort();
560
- const controller = new AbortController();
561
- this.controller = controller;
562
- let normalized;
563
- try {
564
- const result = await project.service.completion(uri, position, context, { signal: controller.signal });
565
- if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
566
- return;
567
- }
568
- normalized = itemsOf(result);
569
- }
570
- catch {
571
- if (request === this.request && this.controller === controller && !controller.signal.aborted) {
572
- this.close();
573
- }
574
- return;
575
- }
576
- const { items, incomplete } = normalized;
577
- this.pool = items;
578
- this.incomplete = incomplete;
579
- this.resolved.clear();
580
- this.server = project.host.serverNames?.(uri)[0] ?? '';
581
- if (!refresh) {
582
- const caret = editor.getCaret();
583
- const lineBefore = textBeforeOf(editor, caret);
584
- const prefix = context.triggerKind === 2 ? '' : identifierPrefix(lineBefore);
585
- this.start = { line: caret.line, character: caret.character - prefix.length };
586
- this.active = 0;
587
- this.touched = false;
588
- }
589
- else if (this.touched && this.ranked[this.active] !== undefined) {
590
- // The list is made again, so the row that was chosen is found again by what it says.
591
- this.activeKey = RecentChoices.keyOf(this.ranked[this.active].item);
592
- }
593
- const asked = this.explicit && !refresh;
604
+ if (!this.isOpen || event.ctrlKey || event.metaKey || event.altKey) {
605
+ return false;
606
+ }
607
+ if (this.commits(event)) {
608
+ void this.accept(false, this.active, event.key);
609
+ return true;
610
+ }
611
+ switch (event.key) {
612
+ case 'ArrowDown':
613
+ this.move(1);
614
+ return true;
615
+ case 'ArrowUp':
616
+ this.move(-1);
617
+ return true;
618
+ case 'PageDown':
619
+ this.move(PAGE);
620
+ return true;
621
+ case 'PageUp':
622
+ this.move(-PAGE);
623
+ return true;
624
+ case 'Enter':
625
+ case 'Tab':
626
+ if (event.shiftKey) {
627
+ return false;
628
+ }
629
+ void this.accept(event.key === 'Tab');
630
+ return true;
631
+ case 'Escape':
632
+ this.close();
633
+ return true;
634
+ default:
635
+ return false;
636
+ }
637
+ }
638
+ /* A character the active item commits on, typed once the person has moved in the list or asked for it, which is when Enter would not be the way they chose. */
639
+ commits(event) {
640
+ if (!this.touched && !this.explicit || event.key.length !== 1 || event.isComposing) {
641
+ return false;
642
+ }
643
+ return this.ranked[this.active]?.item.commitCharacters?.includes(event.key) === true;
644
+ }
645
+ edited(change) {
646
+ this.textRevision++;
647
+ if (change.source !== 'input' || change.changes.length !== 1) {
648
+ this.close();
649
+ return;
650
+ }
651
+ const {
652
+ range,
653
+ text
654
+ } = change.changes[0];
655
+ const triggers = this.triggerCharacters();
656
+ const last = Array.from(text).at(-1) ?? '';
657
+ const typedIdentifier = text !== '' && Array.from(text).every(isIdentifierCharacter);
658
+ if (text.length === 1 && triggers.includes(last)) {
659
+ this.close();
660
+ this.beginRequest({
661
+ triggerKind: 2,
662
+ triggerCharacter: last
663
+ }, 0);
664
+ } else if (isEmptyRange(range) && opensCompletion(textBeforeOf(this.language.editor, range.start), text)) {
665
+ this.close();
666
+ this.beginRequest({
667
+ triggerKind: 1
668
+ }, START_DELAY_MS);
669
+ } else if (this.isOpen) {
670
+ if (text === '' || typedIdentifier) {
594
671
  this.refilter();
595
- if (asked && !this.isOpen && !this.incomplete) {
596
- this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.completion.none') });
597
- }
598
- else if (asked && this.ranked.length === 1 && !this.incomplete) {
599
- // One answer to a list that was asked for is the one wanted.
600
- void this.accept(false, 0);
601
- }
672
+ } else {
673
+ this.close();
674
+ }
675
+ } else if (text.length === 1 && typedIdentifier && isEmptyRange(range)) {
676
+ this.beginRequest({
677
+ triggerKind: 1
678
+ }, START_DELAY_MS);
679
+ } else if (text !== '') {
680
+ // A request the last letter started, still waiting or out, would open the list after a character that ends the word.
681
+ this.close();
602
682
  }
603
- refilter() {
604
- const { editor } = this.language;
605
- const caret = editor.getCaret();
606
- if (this.start === null || caret.line !== this.start.line || caret.character < this.start.character) {
607
- this.close();
608
- return;
609
- }
610
- const lineBefore = textBeforeOf(editor, caret);
611
- const typedKey = `${caret.line}:${caret.character}:${lineBefore}`;
612
- if (this.rankedFor?.pool === this.pool && this.rankedFor.key === typedKey) {
613
- return;
614
- }
615
- this.rankedFor = { pool: this.pool, key: typedKey };
616
- // Most items of a list replace the same range, so the text under it is read once and not once per item.
617
- const typed = new Map();
618
- const textBetween = (range) => {
619
- const key = `${range.start.line}:${range.start.character}:${range.end.line}:${range.end.character}`;
620
- let text = typed.get(key);
621
- if (text === undefined) {
622
- text = editor.textInRange(range);
623
- typed.set(key, text);
624
- }
625
- return text;
626
- };
627
- const before = this.ranked[this.active]?.item;
628
- const recent = recentChoicesOf(this.language.languageId);
629
- this.ranked = rankCompletions(this.pool, (item) => prefixFor(item, caret, lineBefore, textBetween), (item) => recent.recency(item));
630
- if (this.ranked.length === 0) {
631
- if (this.incomplete && this.pool.length > 0) {
632
- this.beginRequest({ triggerKind: 3 }, START_DELAY_MS);
633
- }
634
- this.close();
635
- return;
636
- }
637
- const key = this.activeKey ?? (this.touched && before !== undefined ? RecentChoices.keyOf(before) : null);
638
- this.activeKey = null;
639
- const kept = key === null ? -1 : this.ranked.findIndex((entry) => RecentChoices.keyOf(entry.item) === key);
640
- this.active = kept >= 0 ? kept : 0;
641
- this.touched = this.touched && kept >= 0;
642
- this.publish();
643
- this.scheduleResolve();
644
- if (this.incomplete) {
645
- this.beginRequest({ triggerKind: 3 }, START_DELAY_MS);
646
- }
683
+ }
684
+ caretMoved(position) {
685
+ if (this.start === null) {
686
+ return;
647
687
  }
648
- scheduleResolve() {
649
- const { project, uri } = this.language;
650
- this.timers.clear(this.resolveTimer);
651
- this.resolving?.abort();
652
- const entry = this.ranked[this.active];
653
- if (entry === undefined || this.resolved.has(entry.item) || project.service.providerOptions('completionItem/resolve', uri) === undefined) {
654
- return;
655
- }
656
- const request = this.request;
657
- this.resolveTimer = this.timers.set(() => {
658
- const controller = new AbortController();
659
- this.resolving = controller;
660
- void project.service
661
- .resolveCompletion(uri, entry.item, { signal: controller.signal })
662
- .then((item) => {
663
- this.resolved.set(entry.item, { ...entry.item, ...item });
664
- if (this.ranked[this.active]?.item === entry.item) {
665
- this.publish();
666
- }
667
- })
668
- .catch((error) => {
669
- // A server only resolves an item for the text it was made for, so only an item that is wanted and stale costs a new list.
670
- if (error instanceof StaleResultError && !controller.signal.aborted && request === this.request && this.start !== null) {
671
- void this.ask({ triggerKind: 1 }, request, true);
672
- }
673
- });
674
- }, RESOLVE_DELAY_MS);
675
- }
676
- publish() {
677
- const entry = this.ranked[this.active];
678
- if (entry === undefined || this.start === null) {
679
- return;
680
- }
681
- const item = this.resolved.get(entry.item) ?? entry.item;
682
- const descriptions = qualifiersOf(this.ranked.map((ranked) => ranked.item));
683
- const rows = this.ranked.map(({ item: row, prefix }, index) => ({
684
- label: row.label,
685
- kind: row.kind,
686
- detail: row.labelDetails?.detail ?? '',
687
- description: descriptions[index],
688
- matches: matchedCharacters(row.label, prefix),
689
- deprecated: row.deprecated === true || row.tags?.includes(1) === true
690
- }));
691
- const highlightLanguage = shikiLanguageOf(this.language.languageId);
692
- const view = {
693
- anchor: this.start,
694
- rows,
695
- active: this.active,
696
- detailsOpen: this.detailsOpen,
697
- docs: completionDocsOf(item, highlightLanguage),
698
- server: this.server,
699
- highlightLanguage
700
- };
701
- this.language.popups.setState({ completion: view });
688
+ if (position.line !== this.start.line || position.character < this.start.character) {
689
+ this.close();
690
+ } else {
691
+ this.refilter();
702
692
  }
703
- }
693
+ }
694
+ triggerCharacters() {
695
+ const {
696
+ project,
697
+ uri
698
+ } = this.language;
699
+ return project.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
700
+ }
701
+ beginRequest(context, delay) {
702
+ const {
703
+ project,
704
+ uri
705
+ } = this.language;
706
+ if (!project.service.supports(METHOD, uri)) {
707
+ return;
708
+ }
709
+ this.timers.clear(this.requestTimer);
710
+ const request = ++this.request;
711
+ this.requestTimer = this.timers.set(() => void this.ask(context, request), delay);
712
+ }
713
+ async ask(context, request, refresh = false) {
714
+ const {
715
+ editor,
716
+ project,
717
+ uri
718
+ } = this.language;
719
+ const position = editor.getCaret();
720
+ this.controller?.abort();
721
+ const controller = new AbortController();
722
+ this.controller = controller;
723
+ let normalized;
724
+ try {
725
+ const result = await project.service.completion(uri, position, context, {
726
+ signal: controller.signal
727
+ });
728
+ if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
729
+ return;
730
+ }
731
+ normalized = itemsOf(result);
732
+ } catch {
733
+ if (request === this.request && this.controller === controller && !controller.signal.aborted) {
734
+ this.close();
735
+ }
736
+ return;
737
+ }
738
+ const {
739
+ items,
740
+ incomplete
741
+ } = normalized;
742
+ this.pool = items;
743
+ this.incomplete = incomplete;
744
+ this.resolved.clear();
745
+ this.server = project.host.serverNames?.(uri)[0] ?? '';
746
+ if (!refresh) {
747
+ const caret = editor.getCaret();
748
+ const lineBefore = textBeforeOf(editor, caret);
749
+ const prefix = context.triggerKind === 2 ? '' : identifierPrefix(lineBefore);
750
+ this.start = {
751
+ line: caret.line,
752
+ character: caret.character - prefix.length
753
+ };
754
+ this.active = 0;
755
+ this.touched = false;
756
+ } else if (this.touched && this.ranked[this.active] !== undefined) {
757
+ // The list is made again, so the row that was chosen is found again by what it says.
758
+ this.activeKey = RecentChoices.keyOf(this.ranked[this.active].item);
759
+ }
760
+ const asked = this.explicit && !refresh;
761
+ this.refilter();
762
+ if (asked && !this.isOpen && !this.incomplete) {
763
+ this.language.project.host.notify?.({
764
+ id: TOAST_ID,
765
+ kind: 'error',
766
+ title: this.language.project.i18n.t('editor:language.completion.none')
767
+ });
768
+ } else if (asked && this.ranked.length === 1 && !this.incomplete) {
769
+ // One answer to a list that was asked for is the one wanted.
770
+ void this.accept(false, 0);
771
+ }
772
+ }
773
+ refilter() {
774
+ const {
775
+ editor
776
+ } = this.language;
777
+ const caret = editor.getCaret();
778
+ if (this.start === null || caret.line !== this.start.line || caret.character < this.start.character) {
779
+ this.close();
780
+ return;
781
+ }
782
+ const lineBefore = textBeforeOf(editor, caret);
783
+ const typedKey = `${caret.line}:${caret.character}:${lineBefore}`;
784
+ if (this.rankedFor?.pool === this.pool && this.rankedFor.key === typedKey) {
785
+ return;
786
+ }
787
+ this.rankedFor = {
788
+ pool: this.pool,
789
+ key: typedKey
790
+ };
791
+ // Most items of a list replace the same range, so the text under it is read once and not once per item.
792
+ const typed = new Map();
793
+ const textBetween = range => {
794
+ const key = `${range.start.line}:${range.start.character}:${range.end.line}:${range.end.character}`;
795
+ let text = typed.get(key);
796
+ if (text === undefined) {
797
+ text = editor.textInRange(range);
798
+ typed.set(key, text);
799
+ }
800
+ return text;
801
+ };
802
+ const before = this.ranked[this.active]?.item;
803
+ const recent = recentChoicesOf(this.language.languageId);
804
+ this.ranked = rankCompletions(this.pool, item => prefixFor(item, caret, lineBefore, textBetween), item => recent.recency(item));
805
+ if (this.ranked.length === 0) {
806
+ if (this.incomplete && this.pool.length > 0) {
807
+ this.beginRequest({
808
+ triggerKind: 3
809
+ }, START_DELAY_MS);
810
+ }
811
+ this.close();
812
+ return;
813
+ }
814
+ const key = this.activeKey ?? (this.touched && before !== undefined ? RecentChoices.keyOf(before) : null);
815
+ this.activeKey = null;
816
+ const kept = key === null ? -1 : this.ranked.findIndex(entry => RecentChoices.keyOf(entry.item) === key);
817
+ this.active = kept >= 0 ? kept : 0;
818
+ this.touched = this.touched && kept >= 0;
819
+ this.publish();
820
+ this.scheduleResolve();
821
+ if (this.incomplete) {
822
+ this.beginRequest({
823
+ triggerKind: 3
824
+ }, START_DELAY_MS);
825
+ }
826
+ }
827
+ scheduleResolve() {
828
+ const {
829
+ project,
830
+ uri
831
+ } = this.language;
832
+ this.timers.clear(this.resolveTimer);
833
+ this.resolving?.abort();
834
+ const entry = this.ranked[this.active];
835
+ if (entry === undefined || this.resolved.has(entry.item) || project.service.providerOptions('completionItem/resolve', uri) === undefined) {
836
+ return;
837
+ }
838
+ const request = this.request;
839
+ this.resolveTimer = this.timers.set(() => {
840
+ const controller = new AbortController();
841
+ this.resolving = controller;
842
+ void project.service.resolveCompletion(uri, entry.item, {
843
+ signal: controller.signal
844
+ }).then(item => {
845
+ this.resolved.set(entry.item, {
846
+ ...entry.item,
847
+ ...item
848
+ });
849
+ if (this.ranked[this.active]?.item === entry.item) {
850
+ this.publish();
851
+ }
852
+ }).catch(error => {
853
+ // A server only resolves an item for the text it was made for, so only an item that is wanted and stale costs a new list.
854
+ if (error instanceof StaleResultError && !controller.signal.aborted && request === this.request && this.start !== null) {
855
+ void this.ask({
856
+ triggerKind: 1
857
+ }, request, true);
858
+ }
859
+ });
860
+ }, RESOLVE_DELAY_MS);
861
+ }
862
+ publish() {
863
+ const entry = this.ranked[this.active];
864
+ if (entry === undefined || this.start === null) {
865
+ return;
866
+ }
867
+ const item = this.resolved.get(entry.item) ?? entry.item;
868
+ const descriptions = qualifiersOf(this.ranked.map(ranked => ranked.item));
869
+ const rows = this.ranked.map(({
870
+ item: row,
871
+ prefix
872
+ }, index) => ({
873
+ label: row.label,
874
+ kind: row.kind,
875
+ detail: row.labelDetails?.detail ?? '',
876
+ description: descriptions[index],
877
+ matches: matchedCharacters(row.label, prefix),
878
+ deprecated: row.deprecated === true || row.tags?.includes(1) === true
879
+ }));
880
+ const highlightLanguage = shikiLanguageOf(this.language.languageId);
881
+ const view = {
882
+ anchor: this.start,
883
+ rows,
884
+ active: this.active,
885
+ detailsOpen: this.detailsOpen,
886
+ docs: completionDocsOf(item, highlightLanguage),
887
+ server: this.server,
888
+ highlightLanguage
889
+ };
890
+ this.language.popups.setState({
891
+ completion: view
892
+ });
893
+ }
894
+ }