@adecore/editor-react 0.22.1 → 0.23.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 (196) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +88 -0
  3. package/dist/CompletionPopup.js +11 -3
  4. package/dist/DiffView.d.ts +22 -0
  5. package/dist/DiffView.js +203 -0
  6. package/dist/EditorContextMenu.js +1 -0
  7. package/dist/EditorView.js +18 -1
  8. package/dist/FindReplace.d.ts +1 -1
  9. package/dist/FindReplace.js +41 -11
  10. package/dist/HoverCard.js +2 -2
  11. package/dist/LanguagePopups.js +4 -1
  12. package/dist/Markdown.d.ts +2 -1
  13. package/dist/Markdown.js +8 -4
  14. package/dist/PeekPanel.d.ts +3 -0
  15. package/dist/PeekPanel.js +2 -3
  16. package/dist/PickPopup.js +15 -2
  17. package/dist/RenameCard.js +10 -5
  18. package/dist/RenderedDocs.d.ts +9 -0
  19. package/dist/RenderedDocs.js +84 -0
  20. package/dist/SplitDiffView.d.ts +9 -0
  21. package/dist/SplitDiffView.js +276 -0
  22. package/dist/SymbolPicker.d.ts +3 -0
  23. package/dist/SymbolPicker.js +4 -3
  24. package/dist/call-hierarchy-model.d.ts +52 -0
  25. package/dist/call-hierarchy-model.js +99 -0
  26. package/dist/call-hierarchy.d.ts +42 -0
  27. package/dist/call-hierarchy.js +399 -0
  28. package/dist/code-actions-model.d.ts +2 -0
  29. package/dist/code-actions-model.js +12 -10
  30. package/dist/code-actions.js +3 -1
  31. package/dist/completion-model.d.ts +14 -4
  32. package/dist/completion-model.js +40 -33
  33. package/dist/completion.d.ts +4 -0
  34. package/dist/completion.js +257 -42
  35. package/dist/diagnostics.js +8 -6
  36. package/dist/diff-expansion.d.ts +9 -0
  37. package/dist/diff-expansion.js +41 -0
  38. package/dist/diff-overlays.d.ts +15 -0
  39. package/dist/diff-overlays.js +28 -0
  40. package/dist/diff-syntax.d.ts +16 -0
  41. package/dist/diff-syntax.js +49 -0
  42. package/dist/diff-wordmarks.d.ts +5 -0
  43. package/dist/diff-wordmarks.js +26 -0
  44. package/dist/doc-comment-model.d.ts +23 -0
  45. package/dist/doc-comment-model.js +291 -0
  46. package/dist/editor-language.d.ts +13 -1
  47. package/dist/editor-language.js +14 -4
  48. package/dist/editor-react.css +79 -0
  49. package/dist/format-on-type.d.ts +14 -0
  50. package/dist/format-on-type.js +96 -0
  51. package/dist/host-types.d.ts +17 -0
  52. package/dist/hover.d.ts +4 -0
  53. package/dist/hover.js +15 -2
  54. package/dist/index.d.ts +19 -5
  55. package/dist/index.js +9 -1
  56. package/dist/inlay-hints.d.ts +47 -2
  57. package/dist/inlay-hints.js +353 -16
  58. package/dist/language-ids.js +4 -2
  59. package/dist/locales/en.json +114 -3
  60. package/dist/locales/nl.json +114 -3
  61. package/dist/models.d.ts +6 -2
  62. package/dist/models.js +5 -1
  63. package/dist/patch-decorations.d.ts +27 -0
  64. package/dist/patch-decorations.js +182 -0
  65. package/dist/patch-layout.d.ts +40 -0
  66. package/dist/patch-layout.js +304 -0
  67. package/dist/peek.js +1 -0
  68. package/dist/pick.d.ts +1 -0
  69. package/dist/pick.js +11 -1
  70. package/dist/popups.d.ts +17 -0
  71. package/dist/popups.js +1 -0
  72. package/dist/project-language.d.ts +8 -0
  73. package/dist/project-language.js +34 -2
  74. package/dist/refresher.d.ts +1 -0
  75. package/dist/refresher.js +5 -0
  76. package/dist/rename-model.d.ts +1 -1
  77. package/dist/rename-model.js +3 -7
  78. package/dist/rename.js +5 -8
  79. package/dist/rendered-docs.d.ts +62 -0
  80. package/dist/rendered-docs.js +368 -0
  81. package/dist/row-host.d.ts +2 -0
  82. package/dist/row-host.js +11 -3
  83. package/dist/semantic-delta.d.ts +2 -0
  84. package/dist/semantic-delta.js +27 -0
  85. package/dist/semantic-model.js +16 -0
  86. package/dist/semantic-tokens.d.ts +26 -1
  87. package/dist/semantic-tokens.js +174 -14
  88. package/dist/shortcut-keys.d.ts +2 -0
  89. package/dist/shortcut-keys.js +6 -2
  90. package/dist/signature.js +1 -1
  91. package/dist/snippet-field-transforms.d.ts +30 -0
  92. package/dist/snippet-field-transforms.js +116 -0
  93. package/dist/snippet-indent.d.ts +2 -0
  94. package/dist/snippet-indent.js +49 -0
  95. package/dist/snippet-range-order.d.ts +3 -0
  96. package/dist/snippet-range-order.js +19 -0
  97. package/dist/snippet-session.d.ts +42 -6
  98. package/dist/snippet-session.js +650 -53
  99. package/dist/snippet-transform-client.d.ts +19 -0
  100. package/dist/snippet-transform-client.js +66 -0
  101. package/dist/snippet-transform-format.d.ts +15 -0
  102. package/dist/snippet-transform-format.js +302 -0
  103. package/dist/snippet-transform-protocol.d.ts +45 -0
  104. package/dist/snippet-transform-protocol.js +8 -0
  105. package/dist/snippet-transform-queue.d.ts +32 -0
  106. package/dist/snippet-transform-queue.js +173 -0
  107. package/dist/snippet-transform-worker.d.ts +10 -0
  108. package/dist/snippet-transform-worker.js +41 -0
  109. package/dist/snippet-variable-names.d.ts +1 -0
  110. package/dist/snippet-variable-names.js +36 -0
  111. package/dist/snippet-variables.d.ts +7 -0
  112. package/dist/snippet-variables.js +66 -0
  113. package/dist/snippet.d.ts +30 -3
  114. package/dist/snippet.js +402 -87
  115. package/dist/split-alignment.d.ts +6 -0
  116. package/dist/split-alignment.js +38 -0
  117. package/dist/split-scroll.d.ts +14 -0
  118. package/dist/split-scroll.js +45 -0
  119. package/dist/symbols.d.ts +2 -1
  120. package/dist/symbols.js +39 -7
  121. package/dist/testing.d.ts +5 -1
  122. package/dist/testing.js +12 -0
  123. package/dist/workspace-edit.d.ts +25 -3
  124. package/dist/workspace-edit.js +282 -143
  125. package/package.json +10 -5
  126. package/src/CallHierarchyPanel.tsx +238 -0
  127. package/src/CompletionPopup.tsx +12 -1
  128. package/src/DiffView.tsx +262 -0
  129. package/src/EditorContextMenu.tsx +1 -0
  130. package/src/EditorView.tsx +21 -2
  131. package/src/FindReplace.tsx +51 -13
  132. package/src/HoverCard.tsx +23 -21
  133. package/src/LanguagePopups.tsx +5 -0
  134. package/src/Markdown.tsx +10 -3
  135. package/src/PeekPanel.tsx +2 -3
  136. package/src/PickPopup.tsx +18 -2
  137. package/src/RenameCard.tsx +18 -0
  138. package/src/RenderedDocs.tsx +155 -0
  139. package/src/SplitDiffView.tsx +337 -0
  140. package/src/SymbolPicker.tsx +4 -3
  141. package/src/call-hierarchy-model.ts +158 -0
  142. package/src/call-hierarchy.ts +441 -0
  143. package/src/code-actions-model.ts +17 -11
  144. package/src/code-actions.ts +3 -1
  145. package/src/completion-model.ts +61 -35
  146. package/src/completion.ts +302 -42
  147. package/src/diagnostics.ts +8 -6
  148. package/src/diff-expansion.ts +49 -0
  149. package/src/diff-overlays.tsx +69 -0
  150. package/src/diff-syntax.ts +67 -0
  151. package/src/diff-wordmarks.ts +31 -0
  152. package/src/doc-comment-model.ts +336 -0
  153. package/src/editor-language.ts +21 -5
  154. package/src/editor-react.css +79 -0
  155. package/src/format-on-type.ts +111 -0
  156. package/src/host-types.ts +19 -0
  157. package/src/hover.ts +18 -3
  158. package/src/index.ts +27 -4
  159. package/src/inlay-hints.ts +394 -18
  160. package/src/language-ids.ts +4 -2
  161. package/src/locales/en.json +114 -3
  162. package/src/locales/nl.json +114 -3
  163. package/src/models.ts +40 -1
  164. package/src/patch-decorations.ts +224 -0
  165. package/src/patch-layout.ts +388 -0
  166. package/src/peek.ts +1 -0
  167. package/src/pick.ts +12 -1
  168. package/src/popups.ts +24 -0
  169. package/src/project-language.ts +43 -2
  170. package/src/refresher.ts +6 -0
  171. package/src/rename-model.ts +4 -8
  172. package/src/rename.ts +5 -8
  173. package/src/rendered-docs.ts +422 -0
  174. package/src/row-host.ts +12 -3
  175. package/src/semantic-delta.ts +29 -0
  176. package/src/semantic-model.ts +17 -0
  177. package/src/semantic-tokens.ts +202 -19
  178. package/src/shortcut-keys.ts +6 -2
  179. package/src/signature.ts +1 -1
  180. package/src/snippet-field-transforms.ts +144 -0
  181. package/src/snippet-indent.ts +53 -0
  182. package/src/snippet-range-order.ts +19 -0
  183. package/src/snippet-session.ts +797 -56
  184. package/src/snippet-transform-client.ts +84 -0
  185. package/src/snippet-transform-format.ts +325 -0
  186. package/src/snippet-transform-protocol.ts +24 -0
  187. package/src/snippet-transform-queue.ts +199 -0
  188. package/src/snippet-transform-worker.ts +58 -0
  189. package/src/snippet-variable-names.ts +36 -0
  190. package/src/snippet-variables.ts +74 -0
  191. package/src/snippet.ts +452 -89
  192. package/src/split-alignment.ts +45 -0
  193. package/src/split-scroll.ts +54 -0
  194. package/src/symbols.ts +43 -7
  195. package/src/testing.ts +25 -0
  196. package/src/workspace-edit.ts +347 -169
@@ -18,12 +18,16 @@ export declare class CompletionFeature {
18
18
  private requestTimer;
19
19
  private refreshTimer;
20
20
  private resolveTimer;
21
+ private textRevision;
22
+ private acceptSequence;
23
+ private disposed;
21
24
  private readonly resolved;
22
25
  constructor(language: EditorLanguage, timers?: Timers);
23
26
  get isOpen(): boolean;
24
27
  invoke(): void;
25
28
  move(delta: number): void;
26
29
  accept(replace: boolean, index?: number, commit?: string): Promise<void>;
30
+ private refuseLargeCompletion;
27
31
  private placeMirroredCarets;
28
32
  private surroundings;
29
33
  private afterInsert;
@@ -1,7 +1,13 @@
1
- import { StaleResultError } from '@adecore/lsp';
1
+ import { SnippetTransformQueueError } from './snippet-transform-queue.js';
2
+ import { SnippetTransformError } from './snippet-transform-client.js';
3
+ import { MAX_TRANSFORM_JOBS, MAX_TRANSFORM_WORK } from './snippet-transform-protocol.js';
4
+ import { fileUriToPath, StaleResultError } from '@adecore/lsp';
2
5
  import { comparePositions, shiftPosition } from './diagnostics-model.js';
3
- import { completionDocsOf, identifierPrefix, insertionOf, isIdentifierCharacter, itemsOf, matchedCharacters, mirroredInsertions, opensCompletion, prefixFor, qualifiersOf, rankCompletions } from './completion-model.js';
6
+ import { completionDocsOf, editRangeOf, identifierPrefix, insertionOf, isIdentifierCharacter, itemsOf, matchedCharacters, mirroredInsertions, opensCompletion, prefixFor, qualifiersOf, rankCompletions } from './completion-model.js';
4
7
  import { PARAMETER_HINTS_COMMAND, isCallItem, planCall, withParentheses } from './completion-call.js';
8
+ import { compileSnippet } from './snippet.js';
9
+ import { createSnippetResolver } from './snippet-variables.js';
10
+ import { wordRangeAt } from './rename-model.js';
5
11
  import { RecentChoices, recentChoicesOf } from './recent-choices.js';
6
12
  import { shikiLanguageOf } from './language-ids.js';
7
13
  import { textBeforeOf } from './line-text.js';
@@ -16,6 +22,40 @@ const RESOLVE_DELAY_MS = 120;
16
22
  const PAGE = 8;
17
23
  const LINES_BACK = 60;
18
24
  const TOAST_ID = 'language-completion';
25
+ const MAX_COMPLETION_CHARACTERS = 2_000_000;
26
+ const MAX_COMPLETION_RANGES = 1_000;
27
+ class CompletionLimitError extends Error {
28
+ }
29
+ 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;
58
+ }
19
59
  /*
20
60
  * The suggestions that open while a word is typed. A single typed identifier character or a trigger
21
61
  * character of the server asks for the list; every character after that only filters the list that
@@ -41,6 +81,9 @@ export class CompletionFeature {
41
81
  requestTimer;
42
82
  refreshTimer;
43
83
  resolveTimer;
84
+ textRevision = 0;
85
+ acceptSequence = 0;
86
+ disposed = false;
44
87
  resolved = new Map();
45
88
  constructor(language, timers = realTimers) {
46
89
  this.language = language;
@@ -50,9 +93,13 @@ export class CompletionFeature {
50
93
  editor.onKeyDown((event) => this.key(event)),
51
94
  editor.onTextChange((change) => this.edited(change)),
52
95
  editor.onCaret((position) => this.caretMoved(position)),
53
- editor.onBlur(() => this.close())
96
+ editor.onBlur(() => {
97
+ this.acceptSequence++;
98
+ this.close();
99
+ })
54
100
  ];
55
101
  language.onDispose(() => {
102
+ this.disposed = true;
56
103
  for (const off of offs) {
57
104
  off();
58
105
  }
@@ -64,6 +111,7 @@ export class CompletionFeature {
64
111
  }
65
112
  /* Asks for suggestions at the caret now, as Ctrl+Space does. */
66
113
  invoke() {
114
+ this.language.pick.close();
67
115
  this.explicit = true;
68
116
  this.beginRequest({ triggerKind: 1 }, 0);
69
117
  }
@@ -84,7 +132,28 @@ export class CompletionFeature {
84
132
  if (entry === undefined) {
85
133
  return;
86
134
  }
87
- const { editor, project, uri } = this.language;
135
+ const { editor, project, uri, snippets } = this.language;
136
+ const acceptSequence = ++this.acceptSequence;
137
+ const snippetToken = snippets.token;
138
+ const inSnippet = snippets.isActive;
139
+ const exitChanges = [];
140
+ if (snippets.hasPendingTransforms) {
141
+ this.close();
142
+ if (!(await snippets.settleTransforms(() => !this.disposed && acceptSequence === this.acceptSequence && snippets.token === snippetToken, (changes, map) => exitChanges.push({ changes, map })))) {
143
+ return;
144
+ }
145
+ }
146
+ const acceptedSelections = editor.getSelections();
147
+ const revision = this.textRevision;
148
+ const current = () => {
149
+ if (this.disposed || acceptSequence !== this.acceptSequence || revision !== this.textRevision || (inSnippet && snippets.token !== snippetToken)) {
150
+ return false;
151
+ }
152
+ const selections = editor.getSelections();
153
+ return (selections.length === acceptedSelections.length &&
154
+ selections.every((range, index) => comparePositions(range.start, acceptedSelections[index].start) === 0 &&
155
+ comparePositions(range.end, acceptedSelections[index].end) === 0));
156
+ };
88
157
  recentChoicesOf(this.language.languageId).record(entry.item);
89
158
  let item = this.resolved.get(entry.item) ?? entry.item;
90
159
  const needsResolve = !this.resolved.has(entry.item) && project.service.providerOptions('completionItem/resolve', uri) !== undefined;
@@ -93,44 +162,185 @@ export class CompletionFeature {
93
162
  // Resolving can add the import an item needs; an item that cannot be resolved is still inserted as it is.
94
163
  item = await this.resolveNow(entry.item);
95
164
  }
165
+ if (!current()) {
166
+ return;
167
+ }
168
+ for (const { changes, map } of exitChanges) {
169
+ const mapped = mapCompletionEdits(item, changes, map);
170
+ if (mapped === null) {
171
+ return;
172
+ }
173
+ item = mapped;
174
+ }
96
175
  const caret = editor.getCaret();
97
176
  const lineBefore = textBeforeOf(editor, caret);
98
177
  const lineAfter = editor.textInRange({ start: caret, end: { line: caret.line, character: Number.MAX_SAFE_INTEGER } });
99
- // A character that commits is typed after the name, so a call is not added before it.
100
- const planned = insertionOf(item, caret, lineBefore, replace, commit === undefined ? lineAfter : '(');
101
- const { before, rest } = this.surroundings(planned, caret, lineAfter);
102
- const plan = commit === undefined ? planCall({ item, languageId: this.language.languageId, insertion: planned, before, rest, replace }) : { action: 'none' };
103
- const main = plan.action === 'add' ? withParentheses(planned, plan.inside) : planned;
104
- const extras = (item.additionalTextEdits ?? []).map((edit) => ({ range: edit.range, text: edit.newText }));
105
- // An import above the insertion moves it, and the tab stops are measured from where it ends up.
106
- const start = extras
107
- .filter((extra) => comparePositions(extra.range.end, main.range.start) <= 0)
108
- .sort((left, right) => comparePositions(right.range.start, left.range.start))
109
- .reduce((place, extra) => shiftPosition(place, extra), main.range.start);
110
- // A name typed at several carets is completed at all of them, and the other carets end behind what was put in.
111
178
  const carets = editor.getSelections();
112
- const mirrored = mirroredInsertions(carets, editor.getSelection(), caret, (range) => editor.textInRange(range), main);
113
- editor.applyEdits([{ range: main.range, text: main.text }, ...mirrored, ...extras]);
114
- const landed = editor.getSelections();
115
- if (commit === undefined) {
116
- this.language.snippets.begin(start, main.text, main.stops);
117
- if (plan.action === 'add' && plan.inside && mirrored.length > 0) {
118
- this.placeMirroredCarets(carets, mirrored, landed);
179
+ const source = item.textEdit?.newText ?? item.insertText ?? item.label;
180
+ if (source.length * carets.length > MAX_COMPLETION_CHARACTERS) {
181
+ this.refuseLargeCompletion();
182
+ return;
183
+ }
184
+ let characters = 0;
185
+ let ranges = 0;
186
+ const now = new Date();
187
+ const template = item.insertTextFormat === 2 ? compileSnippet(item.textEdit?.newText ?? item.insertText ?? item.label) : undefined;
188
+ const finishes = new Map();
189
+ const renderAt = (position, selection) => {
190
+ const line = editor.textInRange({ start: { line: position.line, character: 0 }, end: { line: position.line, character: Number.MAX_SAFE_INTEGER } });
191
+ const word = wordRangeAt(line, position);
192
+ const resolver = createSnippetResolver({
193
+ uri,
194
+ languageId: this.language.languageId,
195
+ position,
196
+ selection,
197
+ selectedText: editor.textInRange(selection),
198
+ currentLine: line,
199
+ currentWord: word === null ? '' : editor.textInRange(word),
200
+ cursorIndex: Math.max(0, carets.findIndex((range) => comparePositions(range.start, selection.start) === 0 && comparePositions(range.end, selection.end) === 0)),
201
+ cursorCount: carets.length,
202
+ folder: project.host.folder,
203
+ path: fileUriToPath(uri) ?? project.host.pathOfUri?.(uri) ?? undefined
204
+ }, project.host.resolveSnippetVariable?.bind(project.host), { now });
205
+ const atItem = position === caret || item.textEdit === undefined
206
+ ? item
207
+ : { ...item, textEdit: undefined, insertText: item.textEdit?.newText ?? item.insertText };
208
+ const editStart = editRangeOf(atItem, replace)?.start;
209
+ // A commit character follows the name, so accepting must not also add a call.
210
+ const finish = (variableTransforms) => {
211
+ const insertion = insertionOf(atItem, position, line.slice(0, position.character), replace, commit === undefined ? line.slice(position.character) : '(', {
212
+ template,
213
+ variableTransforms,
214
+ resolve: resolver,
215
+ indentationBefore: editStart === undefined ? undefined : editor.textInRange({ start: { line: editStart.line, character: 0 }, end: editStart })
216
+ });
217
+ return commit === undefined ? insertion : { ...insertion, text: insertion.text + commit };
218
+ };
219
+ const rendered = finish();
220
+ finishes.set(rendered, finish);
221
+ characters += rendered.text.length;
222
+ ranges +=
223
+ (rendered.variableTransforms?.length ?? 0) +
224
+ (rendered.fields?.reduce((count, field) => count + field.ranges.length + (field.linked?.length ?? 0), 1) ?? rendered.stops.length);
225
+ if (rendered.limited || characters > MAX_COMPLETION_CHARACTERS || ranges > MAX_COMPLETION_RANGES) {
226
+ throw new CompletionLimitError();
119
227
  }
120
- if (plan.action === 'enter') {
121
- const end = editor.getCaret();
122
- editor.setCaret({ line: end.line, character: end.character + plan.shift });
228
+ return rendered;
229
+ };
230
+ try {
231
+ const planned = renderAt(caret, editor.getSelection());
232
+ const { before, rest } = this.surroundings(planned, caret, lineAfter);
233
+ const plan = commit === undefined ? planCall({ item, languageId: this.language.languageId, insertion: planned, before, rest, replace }) : { action: 'none' };
234
+ let main = plan.action === 'add' ? withParentheses(planned, plan.inside) : planned;
235
+ const extras = (item.additionalTextEdits ?? []).map((edit) => ({ range: edit.range, text: edit.newText }));
236
+ const instances = new Map();
237
+ const continuing = inSnippet && snippets.isActive && snippetToken === snippets.token;
238
+ const mirrored = mirroredInsertions(carets, editor.getSelection(), caret, (range) => editor.textInRange(range), main, continuing).map((insertion) => {
239
+ const selection = carets.find((range) => range.start.line === insertion.range.start.line &&
240
+ range.start.character >= insertion.range.start.character &&
241
+ range.start.character <= insertion.range.end.character);
242
+ const position = selection[continuing ? 'end' : 'start'];
243
+ const own = renderAt(position, selection);
244
+ const rendered = plan.action === 'add' ? withParentheses(own, plan.inside) : own;
245
+ const edit = { ...insertion, text: rendered.text };
246
+ const instance = { ...rendered, range: insertion.range };
247
+ instances.set(edit, instance);
248
+ const finish = finishes.get(own);
249
+ if (finish !== undefined) {
250
+ finishes.set(instance, (values) => ({ ...finish(values), range: insertion.range }));
251
+ }
252
+ return edit;
253
+ });
254
+ const drafts = [...mirrored.map((edit) => instances.get(edit)), main];
255
+ const jobs = [];
256
+ const keys = new Map();
257
+ const slots = drafts.map((draft) => (draft.variableTransforms ?? []).map((job) => {
258
+ const key = JSON.stringify(job);
259
+ let slot = keys.get(key);
260
+ if (slot === undefined) {
261
+ slot = jobs.length;
262
+ keys.set(key, slot);
263
+ jobs.push(job);
264
+ }
265
+ return slot;
266
+ }));
267
+ if (jobs.length > MAX_TRANSFORM_JOBS || jobs.reduce((sum, job) => sum + job.value.length, 0) > MAX_TRANSFORM_WORK) {
268
+ throw new CompletionLimitError();
123
269
  }
270
+ if (jobs.length > 0 && project.host.snippetTransformWorker !== undefined) {
271
+ const values = await snippets.transformVariables(jobs, current);
272
+ if (!current()) {
273
+ return;
274
+ }
275
+ if (values !== null) {
276
+ const transformed = drafts.map((draft, index) => slots[index].length === 0 ? draft : finishes.get(draft)(slots[index].map((slot) => values[slot])));
277
+ if (transformed.some((draft) => draft.limited) ||
278
+ transformed.reduce((sum, draft) => sum + draft.text.length, 0) > MAX_COMPLETION_CHARACTERS) {
279
+ throw new CompletionLimitError();
280
+ }
281
+ main = transformed.at(-1);
282
+ mirrored.forEach((edit, index) => {
283
+ edit.text = transformed[index].text;
284
+ instances.set(edit, transformed[index]);
285
+ });
286
+ }
287
+ }
288
+ const primary = { range: main.range, text: main.text };
289
+ const changes = [primary, ...mirrored, ...extras];
290
+ if (changes.reduce((total, edit) => total + edit.text.length, 0) > MAX_COMPLETION_CHARACTERS) {
291
+ throw new CompletionLimitError();
292
+ }
293
+ // Imports and earlier carets move each insertion independently; the primary instance stays last.
294
+ const starts = [...mirrored, primary].map((insertion) => changes
295
+ .filter((change) => change !== insertion && comparePositions(change.range.end, insertion.range.start) <= 0)
296
+ .sort((left, right) => comparePositions(right.range.start, left.range.start))
297
+ .reduce((place, change) => shiftPosition(place, change), insertion.range.start));
298
+ const applied = inSnippet ? snippets.applyEdits(changes, snippetToken, extras) : editor.applyEdits(changes);
299
+ if (!applied) {
300
+ return;
301
+ }
302
+ const landed = editor.getSelections();
303
+ if (commit === undefined) {
304
+ this.language.snippets.beginInstances([...mirrored, primary].map((insertion, index) => {
305
+ const rendered = insertion === primary ? main : instances.get(insertion);
306
+ return { start: starts[index], text: rendered.text, stops: rendered.stops, fields: rendered.fields };
307
+ }), { nested: continuing && snippets.isActive });
308
+ if (plan.action === 'add' && plan.inside && mirrored.length > 0 && main.fields === undefined) {
309
+ this.placeMirroredCarets(carets, mirrored, landed);
310
+ }
311
+ if (plan.action === 'enter') {
312
+ const end = editor.getCaret();
313
+ editor.setCaret({ line: end.line, character: end.character + plan.shift });
314
+ }
315
+ }
316
+ else {
317
+ if (this.triggerCharacters().includes(commit)) {
318
+ this.beginRequest({ triggerKind: 2, triggerCharacter: commit }, 0);
319
+ }
320
+ }
321
+ this.afterInsert(item, commit, lineBefore);
322
+ editor.focus();
124
323
  }
125
- else {
126
- const end = editor.getCaret();
127
- editor.applyEdits([{ range: { start: end, end }, text: commit }]);
128
- if (this.triggerCharacters().includes(commit)) {
129
- this.beginRequest({ triggerKind: 2, triggerCharacter: commit }, 0);
324
+ catch (error) {
325
+ if (!current()) {
326
+ return;
327
+ }
328
+ if (error instanceof SnippetTransformQueueError) {
329
+ project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformBusy') });
330
+ }
331
+ else if (error instanceof CompletionLimitError || (error instanceof SnippetTransformError && error.kind === 'range')) {
332
+ this.refuseLargeCompletion();
333
+ }
334
+ else if (error instanceof SnippetTransformError) {
335
+ project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.transformFailed') });
336
+ }
337
+ else {
338
+ throw error;
130
339
  }
131
340
  }
132
- this.afterInsert(item, commit, lineBefore);
133
- editor.focus();
341
+ }
342
+ refuseLargeCompletion() {
343
+ this.language.project.host.notify?.({ id: TOAST_ID, kind: 'error', title: this.language.project.i18n.t('editor:language.completion.tooLarge') });
134
344
  }
135
345
  /* The carets that got the same `()` go inside it like the primary one, which the snippet already placed. */
136
346
  placeMirroredCarets(before, mirrored, landed) {
@@ -183,7 +393,7 @@ export class CompletionFeature {
183
393
  async resolveNow(item) {
184
394
  const { editor, project, uri } = this.language;
185
395
  try {
186
- return await project.service.resolveCompletion(uri, item);
396
+ return { ...item, ...(await project.service.resolveCompletion(uri, item)) };
187
397
  }
188
398
  catch (error) {
189
399
  if (!(error instanceof StaleResultError)) {
@@ -192,7 +402,7 @@ export class CompletionFeature {
192
402
  }
193
403
  try {
194
404
  const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 })).items.find((candidate) => candidate.label === item.label && candidate.kind === item.kind);
195
- return fresh === undefined ? item : await project.service.resolveCompletion(uri, fresh).catch(() => fresh);
405
+ return fresh === undefined ? item : { ...fresh, ...(await project.service.resolveCompletion(uri, fresh).catch(() => fresh)) };
196
406
  }
197
407
  catch {
198
408
  return item;
@@ -268,6 +478,7 @@ export class CompletionFeature {
268
478
  return this.ranked[this.active]?.item.commitCharacters?.includes(event.key) === true;
269
479
  }
270
480
  edited(change) {
481
+ this.textRevision++;
271
482
  if (change.source !== 'input' || change.changes.length !== 1) {
272
483
  this.close();
273
484
  return;
@@ -340,17 +551,21 @@ export class CompletionFeature {
340
551
  this.controller?.abort();
341
552
  const controller = new AbortController();
342
553
  this.controller = controller;
343
- let result;
554
+ let normalized;
344
555
  try {
345
- result = await project.service.completion(uri, position, context, { signal: controller.signal });
556
+ const result = await project.service.completion(uri, position, context, { signal: controller.signal });
557
+ if (request !== this.request || this.controller !== controller || controller.signal.aborted) {
558
+ return;
559
+ }
560
+ normalized = itemsOf(result);
346
561
  }
347
562
  catch {
563
+ if (request === this.request && this.controller === controller && !controller.signal.aborted) {
564
+ this.close();
565
+ }
348
566
  return;
349
567
  }
350
- if (request !== this.request) {
351
- return;
352
- }
353
- const { items, incomplete } = itemsOf(result);
568
+ const { items, incomplete } = normalized;
354
569
  this.pool = items;
355
570
  this.incomplete = incomplete;
356
571
  this.resolved.clear();
@@ -428,7 +643,7 @@ export class CompletionFeature {
428
643
  void project.service
429
644
  .resolveCompletion(uri, entry.item)
430
645
  .then((item) => {
431
- this.resolved.set(entry.item, item);
646
+ this.resolved.set(entry.item, { ...entry.item, ...item });
432
647
  if (this.ranked[this.active]?.item === entry.item) {
433
648
  this.publish();
434
649
  }
@@ -1,5 +1,5 @@
1
1
  import { isShortcut } from './shortcut-keys.js';
2
- import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, shiftRange } from './diagnostics-model.js';
2
+ import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, severityOf, shiftRange } from './diagnostics-model.js';
3
3
  /*
4
4
  * The problems the servers report for one file. A report from a server replaces the one before it
5
5
  * from the same server, the editor draws them, and each one follows its text through edits made
@@ -54,7 +54,7 @@ export class DiagnosticsFeature {
54
54
  return false;
55
55
  }
56
56
  const { start } = target.diagnostic.range;
57
- this.reveal(start, comparePositions(start, caret) > 0 ? 'centerDown' : 'centerUp');
57
+ this.reveal(target, comparePositions(start, caret) > 0 ? 'centerDown' : 'centerUp');
58
58
  return true;
59
59
  }
60
60
  /* Puts the caret on the first problem of the file. */
@@ -63,7 +63,7 @@ export class DiagnosticsFeature {
63
63
  if (target === null) {
64
64
  return false;
65
65
  }
66
- this.reveal(target.diagnostic.range.start, 'center');
66
+ this.reveal(target, 'center');
67
67
  return true;
68
68
  }
69
69
  onChange(listener) {
@@ -72,11 +72,13 @@ export class DiagnosticsFeature {
72
72
  this.listeners.delete(listener);
73
73
  };
74
74
  }
75
- reveal(position, reveal) {
76
- const { editor } = this.language;
77
- editor.setCaret(position, reveal);
75
+ reveal(problem, reveal) {
76
+ const { editor, project } = this.language;
77
+ editor.setCaret(problem.diagnostic.range.start, reveal);
78
78
  editor.focus();
79
79
  this.language.hover.showProblemsAt(editor.getCaret());
80
+ const severity = project.i18n.t(`editor:language.announce.severity.${severityOf(problem.diagnostic)}`);
81
+ editor.announce(project.i18n.t('editor:language.announce.problem', { severity, message: problem.diagnostic.message }));
80
82
  }
81
83
  shift(changes) {
82
84
  for (const [server, diagnostics] of this.reports) {
@@ -0,0 +1,9 @@
1
+ import { type DiffDocument } from '@adecore/merge';
2
+ export interface DiffLimits {
3
+ readonly rows: number;
4
+ readonly characters: number;
5
+ readonly lineLength: number;
6
+ }
7
+ export declare const DEFAULT_UNFOLD_LIMITS: DiffLimits;
8
+ export declare function fitsDiff(document: DiffDocument, unfolded: ReadonlyMap<number, number>, limits: DiffLimits, maxRows?: number): boolean;
9
+ export declare function expandDiff(document: DiffDocument, unfolded: ReadonlyMap<number, number>, id: number, limits: DiffLimits): ReadonlyMap<number, number>;
@@ -0,0 +1,41 @@
1
+ import { diffRows } from '@adecore/merge';
2
+ export const DEFAULT_UNFOLD_LIMITS = { rows: 20000, characters: 2000000, lineLength: 10000 };
3
+ export function fitsDiff(document, unfolded, limits, maxRows = limits.rows) {
4
+ let rows = 0;
5
+ let characters = 0;
6
+ for (const row of diffRows(document, 'unified', unfolded)) {
7
+ if (row.kind === 'gap') {
8
+ continue;
9
+ }
10
+ const text = row.new === undefined ? document.sides.old[row.old.index] : document.sides.new[row.new.index];
11
+ characters += text.length + (rows > 0 ? 1 : 0);
12
+ if (++rows > maxRows || characters > limits.characters || text.length > limits.lineLength) {
13
+ return false;
14
+ }
15
+ }
16
+ return true;
17
+ }
18
+ export function expandDiff(document, unfolded, id, limits) {
19
+ const rows = diffRows(document, 'unified', unfolded);
20
+ const gap = rows.find((row) => row.kind === 'gap' && row.id === id);
21
+ if (gap?.kind !== 'gap' || !gap.unfoldable) {
22
+ return unfolded;
23
+ }
24
+ const visible = rows.filter((row) => row.kind === 'line').length;
25
+ const maxRows = Math.min(limits.rows, visible + 500);
26
+ const previous = unfolded.get(id) ?? 0;
27
+ let low = 0;
28
+ let high = Math.min(500, gap.oldCount);
29
+ // Between-hunk gaps reveal from both ends; search the per-end count within the shared row budget.
30
+ while (low < high) {
31
+ const count = Math.ceil((low + high) / 2);
32
+ const candidate = new Map(unfolded).set(id, previous + count);
33
+ if (fitsDiff(document, candidate, limits, maxRows)) {
34
+ low = count;
35
+ }
36
+ else {
37
+ high = count - 1;
38
+ }
39
+ }
40
+ return low === 0 ? unfolded : new Map(unfolded).set(id, previous + low);
41
+ }
@@ -0,0 +1,15 @@
1
+ import type { Editor, EditorEngine } from '@adecore/editor';
2
+ import type { DiffDisplay } from '@adecore/merge';
3
+ import { RowHost, LineActionHost } from './row-host.ts';
4
+ export interface MountedDiff {
5
+ engine: EditorEngine;
6
+ editor: Editor;
7
+ rows: RowHost;
8
+ actions: LineActionHost;
9
+ }
10
+ export declare function DiffOverlays({ mounted, display, unfold, blocked }: {
11
+ mounted: MountedDiff;
12
+ display: DiffDisplay;
13
+ unfold(id: number): void;
14
+ blocked?: ReadonlySet<number>;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useSyncExternalStore } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Button } from '@adecore/ui';
6
+ import { formatNumber } from '@adecore/ui/format';
7
+ import { RowHost, LineActionHost } from './row-host.js';
8
+ export function DiffOverlays({ mounted, display, unfold, blocked }) {
9
+ const { t } = useTranslation('editor');
10
+ useSyncExternalStore(mounted.rows.subscribe, mounted.rows.getVersion, mounted.rows.getVersion);
11
+ useSyncExternalStore(mounted.actions.subscribe, mounted.actions.getVersion, mounted.actions.getVersion);
12
+ return (_jsxs(_Fragment, { children: [display.gaps.map(({ row }) => {
13
+ const container = mounted.rows.container(`gap-${row.id}`);
14
+ if (container === undefined) {
15
+ return null;
16
+ }
17
+ const text = t('diff.hidden', { count: formatNumber(Math.max(row.oldCount, row.newCount)) });
18
+ return createPortal(_jsx("div", { className: "adecore-editor-diff-gap", children: row.unfoldable ? (_jsx(Button, { size: "xs", disabled: blocked?.has(row.id), title: blocked?.has(row.id) ? t('diff.contextLimit') : undefined, onClick: () => unfold(row.id), children: text })) : (_jsx("span", { children: text })) }), container, `gap-${row.id}`);
19
+ }), display.rows.map((row, index) => {
20
+ if (row.noNewline === undefined) {
21
+ return null;
22
+ }
23
+ const container = mounted.actions.container(`no-newline-${index}`);
24
+ return container === undefined
25
+ ? null
26
+ : createPortal(_jsx("span", { className: "adecore-editor-diff-no-newline", children: t('diff.noNewline') }), container, `no-newline-${index}`);
27
+ })] }));
28
+ }
@@ -0,0 +1,16 @@
1
+ import type { LineToken } from '@adecore/editor';
2
+ import type { DiffSyntaxDocument } from '@adecore/editor/shiki-worker';
3
+ import type { DiffDocument } from '@adecore/merge';
4
+ export type DiffSourceSide = 'old' | 'new';
5
+ export interface DiffSourceTokens {
6
+ get(side: DiffSourceSide, index: number): readonly LineToken[] | null | undefined;
7
+ subscribe(listener: () => void): () => void;
8
+ }
9
+ export interface DiffSyntaxSources {
10
+ readonly documents: Readonly<Record<DiffSourceSide, DiffSyntaxDocument>>;
11
+ locate(side: DiffSourceSide, index: number): {
12
+ readonly block: number;
13
+ readonly line: number;
14
+ } | undefined;
15
+ }
16
+ export declare function diffSyntaxDocument(document: DiffDocument, language: string, theme: string): DiffSyntaxSources;
@@ -0,0 +1,49 @@
1
+ function canTokenize(source) {
2
+ if (source.length > 20000) {
3
+ return false;
4
+ }
5
+ let characters = Math.max(0, source.length - 1);
6
+ for (const line of source) {
7
+ characters += line.length;
8
+ if (line.length > 10000 || characters > 2000000) {
9
+ return false;
10
+ }
11
+ }
12
+ return true;
13
+ }
14
+ export function diffSyntaxDocument(document, language, theme) {
15
+ const locations = { old: [], new: [] };
16
+ const build = (side) => {
17
+ const source = document.sides[side];
18
+ // Skipping a long hidden line loses grammar state; keep the entire side plain instead.
19
+ if (!canTokenize(source)) {
20
+ return { language, theme, blocks: [], resets: [] };
21
+ }
22
+ const blocks = [];
23
+ const resets = [];
24
+ const ranges = document.snapshots
25
+ ? [{ from: 0, to: source.length }]
26
+ : document.offsets.map((offset, index) => ({ from: offset[side], to: document.offsets[index + 1]?.[side] ?? source.length }));
27
+ for (const range of ranges) {
28
+ if (!document.snapshots && range.from < range.to) {
29
+ resets.push(blocks.length);
30
+ }
31
+ for (let from = range.from; from < range.to; from += 200) {
32
+ const to = Math.min(from + 200, range.to);
33
+ const block = blocks.length;
34
+ blocks.push(source.slice(from, to).join('\n'));
35
+ for (let index = from; index < to; index++) {
36
+ locations[side][index] = { block, line: index - from };
37
+ }
38
+ }
39
+ }
40
+ return { language, theme, blocks, resets };
41
+ };
42
+ const documents = { old: build('old'), new: build('new') };
43
+ return {
44
+ documents,
45
+ locate(side, index) {
46
+ return Number.isInteger(index) && index >= 0 ? locations[side][index] : undefined;
47
+ }
48
+ };
49
+ }
@@ -0,0 +1,5 @@
1
+ import { type DiffDocument } from '@adecore/merge';
2
+ export declare function wordMarks(document: DiffDocument): {
3
+ old: Map<number, readonly (readonly [number, number])[]>;
4
+ new: Map<number, readonly (readonly [number, number])[]>;
5
+ };
@@ -0,0 +1,26 @@
1
+ import { diffRows, wordChanges } from '@adecore/merge';
2
+ export function wordMarks(document) {
3
+ const marks = { old: new Map(), new: new Map() };
4
+ let pairs = 0;
5
+ let characters = 0;
6
+ for (const row of diffRows(document, 'split')) {
7
+ if (row.kind !== 'line' || row.change !== 'pair' || document.hunks[row.hunk].coarse) {
8
+ continue;
9
+ }
10
+ const old = document.sides.old[row.old.index];
11
+ const fresh = document.sides.new[row.new.index];
12
+ characters += old.length + fresh.length;
13
+ // Per-pair limits alone still allow thousands of expensive word diffs during one mount.
14
+ if (++pairs > 1000 || characters > 100000) {
15
+ break;
16
+ }
17
+ const words = wordChanges(old, fresh);
18
+ if (words === null) {
19
+ continue;
20
+ }
21
+ const ranges = (text, ranges) => ranges.filter((range) => !document.ignoreWhitespace || /\S/.test(text.slice(range.from, range.to))).map((range) => [range.from, range.to]);
22
+ marks.old.set(row.old.index, ranges(old, words.old));
23
+ marks.new.set(row.new.index, ranges(fresh, words.new));
24
+ }
25
+ return marks;
26
+ }