@adecore/editor-react 0.22.1 → 0.23.1

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 (209) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +92 -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/PanelResizeHandle.d.ts +8 -0
  15. package/dist/PanelResizeHandle.js +71 -0
  16. package/dist/PeekPanel.d.ts +3 -0
  17. package/dist/PeekPanel.js +7 -4
  18. package/dist/PickPopup.js +15 -2
  19. package/dist/RenameCard.js +10 -5
  20. package/dist/RenderedDocs.d.ts +9 -0
  21. package/dist/RenderedDocs.js +84 -0
  22. package/dist/SplitDiffView.d.ts +9 -0
  23. package/dist/SplitDiffView.js +276 -0
  24. package/dist/SymbolPicker.d.ts +3 -0
  25. package/dist/SymbolPicker.js +4 -3
  26. package/dist/call-hierarchy-model.d.ts +52 -0
  27. package/dist/call-hierarchy-model.js +93 -0
  28. package/dist/call-hierarchy.d.ts +42 -0
  29. package/dist/call-hierarchy.js +398 -0
  30. package/dist/code-actions-model.d.ts +2 -0
  31. package/dist/code-actions-model.js +12 -10
  32. package/dist/code-actions.js +3 -1
  33. package/dist/completion-model.d.ts +14 -4
  34. package/dist/completion-model.js +40 -33
  35. package/dist/completion.d.ts +4 -0
  36. package/dist/completion.js +257 -42
  37. package/dist/diagnostics.js +8 -6
  38. package/dist/diff-expansion.d.ts +9 -0
  39. package/dist/diff-expansion.js +41 -0
  40. package/dist/diff-overlays.d.ts +15 -0
  41. package/dist/diff-overlays.js +28 -0
  42. package/dist/diff-syntax.d.ts +16 -0
  43. package/dist/diff-syntax.js +49 -0
  44. package/dist/diff-wordmarks.d.ts +5 -0
  45. package/dist/diff-wordmarks.js +26 -0
  46. package/dist/doc-comment-model.d.ts +23 -0
  47. package/dist/doc-comment-model.js +291 -0
  48. package/dist/editor-language.d.ts +13 -1
  49. package/dist/editor-language.js +15 -5
  50. package/dist/editor-react.css +79 -0
  51. package/dist/format-on-type.d.ts +14 -0
  52. package/dist/format-on-type.js +96 -0
  53. package/dist/host-types.d.ts +19 -0
  54. package/dist/hover.d.ts +4 -0
  55. package/dist/hover.js +15 -2
  56. package/dist/index.d.ts +19 -5
  57. package/dist/index.js +9 -1
  58. package/dist/inlay-hints.d.ts +47 -2
  59. package/dist/inlay-hints.js +353 -16
  60. package/dist/language-ids.js +4 -2
  61. package/dist/locales/en.json +117 -3
  62. package/dist/locales/nl.json +117 -3
  63. package/dist/models.d.ts +6 -2
  64. package/dist/models.js +5 -1
  65. package/dist/panel-size.d.ts +6 -0
  66. package/dist/panel-size.js +25 -0
  67. package/dist/patch-decorations.d.ts +27 -0
  68. package/dist/patch-decorations.js +182 -0
  69. package/dist/patch-layout.d.ts +40 -0
  70. package/dist/patch-layout.js +304 -0
  71. package/dist/peek.d.ts +3 -1
  72. package/dist/peek.js +6 -1
  73. package/dist/pick.d.ts +1 -0
  74. package/dist/pick.js +11 -1
  75. package/dist/popups.d.ts +17 -0
  76. package/dist/popups.js +1 -0
  77. package/dist/project-language.d.ts +12 -0
  78. package/dist/project-language.js +52 -2
  79. package/dist/refresher.d.ts +1 -0
  80. package/dist/refresher.js +5 -0
  81. package/dist/rename-model.d.ts +1 -1
  82. package/dist/rename-model.js +3 -7
  83. package/dist/rename.js +5 -8
  84. package/dist/rendered-docs.d.ts +64 -0
  85. package/dist/rendered-docs.js +374 -0
  86. package/dist/row-host.d.ts +2 -0
  87. package/dist/row-host.js +11 -3
  88. package/dist/semantic-delta.d.ts +2 -0
  89. package/dist/semantic-delta.js +27 -0
  90. package/dist/semantic-model.js +16 -0
  91. package/dist/semantic-tokens.d.ts +26 -1
  92. package/dist/semantic-tokens.js +174 -14
  93. package/dist/shortcut-keys.d.ts +2 -0
  94. package/dist/shortcut-keys.js +6 -2
  95. package/dist/signature.js +1 -1
  96. package/dist/snippet-field-transforms.d.ts +30 -0
  97. package/dist/snippet-field-transforms.js +116 -0
  98. package/dist/snippet-indent.d.ts +2 -0
  99. package/dist/snippet-indent.js +49 -0
  100. package/dist/snippet-range-order.d.ts +3 -0
  101. package/dist/snippet-range-order.js +19 -0
  102. package/dist/snippet-session.d.ts +42 -6
  103. package/dist/snippet-session.js +650 -53
  104. package/dist/snippet-transform-client.d.ts +19 -0
  105. package/dist/snippet-transform-client.js +66 -0
  106. package/dist/snippet-transform-format.d.ts +15 -0
  107. package/dist/snippet-transform-format.js +302 -0
  108. package/dist/snippet-transform-protocol.d.ts +45 -0
  109. package/dist/snippet-transform-protocol.js +8 -0
  110. package/dist/snippet-transform-queue.d.ts +32 -0
  111. package/dist/snippet-transform-queue.js +173 -0
  112. package/dist/snippet-transform-worker.d.ts +10 -0
  113. package/dist/snippet-transform-worker.js +41 -0
  114. package/dist/snippet-variable-names.d.ts +1 -0
  115. package/dist/snippet-variable-names.js +36 -0
  116. package/dist/snippet-variables.d.ts +7 -0
  117. package/dist/snippet-variables.js +66 -0
  118. package/dist/snippet.d.ts +30 -3
  119. package/dist/snippet.js +402 -87
  120. package/dist/split-alignment.d.ts +6 -0
  121. package/dist/split-alignment.js +38 -0
  122. package/dist/split-scroll.d.ts +14 -0
  123. package/dist/split-scroll.js +45 -0
  124. package/dist/symbols.d.ts +2 -1
  125. package/dist/symbols.js +39 -7
  126. package/dist/testing.d.ts +5 -1
  127. package/dist/testing.js +12 -0
  128. package/dist/timers.d.ts +1 -0
  129. package/dist/timers.js +4 -0
  130. package/dist/use-panel-body.d.ts +6 -0
  131. package/dist/use-panel-body.js +13 -0
  132. package/dist/workspace-edit.d.ts +25 -3
  133. package/dist/workspace-edit.js +282 -143
  134. package/package.json +10 -5
  135. package/src/CallHierarchyPanel.tsx +244 -0
  136. package/src/CompletionPopup.tsx +12 -1
  137. package/src/DiffView.tsx +262 -0
  138. package/src/EditorContextMenu.tsx +1 -0
  139. package/src/EditorView.tsx +21 -2
  140. package/src/FindReplace.tsx +51 -13
  141. package/src/HoverCard.tsx +23 -21
  142. package/src/LanguagePopups.tsx +5 -0
  143. package/src/Markdown.tsx +10 -3
  144. package/src/PanelResizeHandle.tsx +104 -0
  145. package/src/PeekPanel.tsx +11 -6
  146. package/src/PickPopup.tsx +18 -2
  147. package/src/RenameCard.tsx +18 -0
  148. package/src/RenderedDocs.tsx +155 -0
  149. package/src/SplitDiffView.tsx +337 -0
  150. package/src/SymbolPicker.tsx +4 -3
  151. package/src/call-hierarchy-model.ts +151 -0
  152. package/src/call-hierarchy.ts +440 -0
  153. package/src/code-actions-model.ts +17 -11
  154. package/src/code-actions.ts +3 -1
  155. package/src/completion-model.ts +61 -35
  156. package/src/completion.ts +302 -42
  157. package/src/diagnostics.ts +8 -6
  158. package/src/diff-expansion.ts +49 -0
  159. package/src/diff-overlays.tsx +69 -0
  160. package/src/diff-syntax.ts +67 -0
  161. package/src/diff-wordmarks.ts +31 -0
  162. package/src/doc-comment-model.ts +336 -0
  163. package/src/editor-language.ts +22 -6
  164. package/src/editor-react.css +79 -0
  165. package/src/format-on-type.ts +111 -0
  166. package/src/host-types.ts +23 -0
  167. package/src/hover.ts +18 -3
  168. package/src/index.ts +27 -4
  169. package/src/inlay-hints.ts +394 -18
  170. package/src/language-ids.ts +4 -2
  171. package/src/locales/en.json +117 -3
  172. package/src/locales/nl.json +117 -3
  173. package/src/models.ts +40 -1
  174. package/src/panel-size.ts +28 -0
  175. package/src/patch-decorations.ts +224 -0
  176. package/src/patch-layout.ts +388 -0
  177. package/src/peek.ts +6 -1
  178. package/src/pick.ts +12 -1
  179. package/src/popups.ts +24 -0
  180. package/src/project-language.ts +63 -2
  181. package/src/refresher.ts +6 -0
  182. package/src/rename-model.ts +4 -8
  183. package/src/rename.ts +5 -8
  184. package/src/rendered-docs.ts +430 -0
  185. package/src/row-host.ts +12 -3
  186. package/src/semantic-delta.ts +29 -0
  187. package/src/semantic-model.ts +17 -0
  188. package/src/semantic-tokens.ts +202 -19
  189. package/src/shortcut-keys.ts +6 -2
  190. package/src/signature.ts +1 -1
  191. package/src/snippet-field-transforms.ts +144 -0
  192. package/src/snippet-indent.ts +53 -0
  193. package/src/snippet-range-order.ts +19 -0
  194. package/src/snippet-session.ts +797 -56
  195. package/src/snippet-transform-client.ts +84 -0
  196. package/src/snippet-transform-format.ts +325 -0
  197. package/src/snippet-transform-protocol.ts +24 -0
  198. package/src/snippet-transform-queue.ts +199 -0
  199. package/src/snippet-transform-worker.ts +58 -0
  200. package/src/snippet-variable-names.ts +36 -0
  201. package/src/snippet-variables.ts +74 -0
  202. package/src/snippet.ts +452 -89
  203. package/src/split-alignment.ts +45 -0
  204. package/src/split-scroll.ts +54 -0
  205. package/src/symbols.ts +43 -7
  206. package/src/testing.ts +25 -0
  207. package/src/timers.ts +5 -0
  208. package/src/use-panel-body.ts +17 -0
  209. package/src/workspace-edit.ts +347 -169
@@ -1,7 +1,8 @@
1
+ import type { SnippetTransformJob } from './snippet-transform-protocol.ts';
1
2
  import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
2
- import type { CompletionItem, CompletionResult, MarkupContent } from '@adecore/lsp';
3
+ import { type CompletionItem, type CompletionResult, type MarkupContent } from '@adecore/lsp';
3
4
  import type { CompletionDocs } from './popups.ts';
4
- import { type SnippetStop } from './snippet.ts';
5
+ import { type SnippetField, type SnippetStop, type SnippetTemplate, type SnippetVariableResolver } from './snippet.ts';
5
6
  export declare function isIdentifierCharacter(character: string): boolean;
6
7
  export declare function identifierPrefix(before: string): string;
7
8
  export declare function opensCompletion(before: string, text: string): boolean;
@@ -34,12 +35,21 @@ export declare const CLASS_KIND = 7;
34
35
  export declare const SNIPPET_FORMAT = 2;
35
36
  export declare function isCallableKind(kind: number | undefined): boolean;
36
37
  export interface Insertion {
38
+ readonly limited?: true;
37
39
  readonly range: EditorRange;
38
40
  readonly text: string;
39
41
  readonly stops: readonly SnippetStop[];
42
+ readonly fields?: readonly SnippetField[];
43
+ readonly variableTransforms?: readonly SnippetTransformJob[];
40
44
  }
41
- export declare function insertionOf(item: CompletionItem, caret: EditorPosition, lineBefore: string, replace: boolean, lineAfter?: string): Insertion;
42
- export declare function mirroredInsertions(carets: readonly EditorRange[], primary: EditorRange, caret: EditorPosition, text: (range: EditorRange) => string, insertion: Insertion): EditorContentChange[];
45
+ export interface SnippetInsertionOptions {
46
+ readonly resolve?: SnippetVariableResolver;
47
+ readonly variableTransforms?: readonly string[];
48
+ readonly template?: SnippetTemplate;
49
+ readonly indentationBefore?: string;
50
+ }
51
+ export declare function insertionOf(item: CompletionItem, caret: EditorPosition, lineBefore: string, replace: boolean, lineAfter?: string, options?: SnippetInsertionOptions): Insertion;
52
+ export declare function mirroredInsertions(carets: readonly EditorRange[], primary: EditorRange, caret: EditorPosition, text: (range: EditorRange) => string, insertion: Insertion, includeSelections?: boolean): EditorContentChange[];
43
53
  export declare function documentationText(documentation: string | MarkupContent | undefined): string;
44
54
  export declare const SOURCE_LINE: RegExp;
45
55
  export declare function completionDocsOf(item: CompletionItem, highlightLanguage: string): CompletionDocs | null;
@@ -1,6 +1,8 @@
1
+ import { applyCompletionDefaults } from '@adecore/lsp';
1
2
  import { comparePositions } from './diagnostics-model.js';
2
3
  import { escapeMarkdown, isEmptyHover, splitSignatures } from './hover-content.js';
3
- import { parseSnippet, tabOrder } from './snippet.js';
4
+ import { compileSnippet, parseSnippet, tabOrder } from './snippet.js';
5
+ import { indentSnippet } from './snippet-indent.js';
4
6
  const IDENTIFIER_CHARACTER = /[\p{L}\p{N}\p{M}_$]/u;
5
7
  export function isIdentifierCharacter(character) {
6
8
  return IDENTIFIER_CHARACTER.test(character);
@@ -117,35 +119,23 @@ export function editRangeOf(item, replace) {
117
119
  }
118
120
  /* Items from a result, with the list's defaults filled in so each carries its own edit range and format. */
119
121
  export function itemsOf(result) {
120
- if (result === null) {
121
- return { items: [], incomplete: false };
122
- }
123
- if (Array.isArray(result)) {
124
- return { items: result, incomplete: false };
125
- }
126
- const list = result;
127
- const defaults = list.itemDefaults;
128
- if (defaults === undefined) {
129
- return { items: list.items, incomplete: list.isIncomplete };
130
- }
122
+ const list = result === null || Array.isArray(result) ? undefined : result;
123
+ const items = result === null ? [] : Array.isArray(result) ? result : result.items;
131
124
  return {
132
- incomplete: list.isIncomplete,
133
- items: list.items.map((item) => {
134
- const next = { ...item };
135
- if (defaults.insertTextFormat !== undefined && next.insertTextFormat === undefined) {
136
- next.insertTextFormat = defaults.insertTextFormat;
125
+ incomplete: list?.isIncomplete ?? false,
126
+ items: items.flatMap((item) => {
127
+ if (typeof item?.label !== 'string') {
128
+ return [];
137
129
  }
138
- if (defaults.commitCharacters !== undefined && next.commitCharacters === undefined) {
139
- next.commitCharacters = defaults.commitCharacters;
130
+ try {
131
+ return [applyCompletionDefaults(item, list?.itemDefaults, { onInvalidRange: () => undefined })];
140
132
  }
141
- if (next.textEdit === undefined && defaults.editRange !== undefined) {
142
- const text = item.textEditText ?? item.insertText ?? item.label;
143
- next.textEdit =
144
- 'start' in defaults.editRange
145
- ? { range: defaults.editRange, newText: text }
146
- : { insert: defaults.editRange.insert, replace: defaults.editRange.replace, newText: text };
133
+ catch (error) {
134
+ if (error instanceof TypeError || error instanceof RangeError) {
135
+ return [];
136
+ }
137
+ throw error;
147
138
  }
148
- return next;
149
139
  })
150
140
  };
151
141
  }
@@ -236,7 +226,7 @@ function withoutCall(snippet) {
236
226
  * What goes into the text for an item and the range it takes the place of, given the caret. `lineAfter` is
237
227
  * the rest of the line after the caret: an item that would add a call does not when a parenthesis follows.
238
228
  */
239
- export function insertionOf(item, caret, lineBefore, replace, lineAfter = '') {
229
+ export function insertionOf(item, caret, lineBefore, replace, lineAfter = '', options = {}) {
240
230
  const raw = item.textEdit?.newText ?? item.insertText ?? item.label;
241
231
  const edit = editRangeOf(item, replace);
242
232
  const range = edit === null
@@ -244,17 +234,34 @@ export function insertionOf(item, caret, lineBefore, replace, lineAfter = '') {
244
234
  : // The word grew since the answer came, so what was typed since belongs to what is replaced.
245
235
  { start: edit.start, end: comparePositions(edit.end, caret) < 0 && edit.end.line === caret.line ? caret : edit.end };
246
236
  if (item.insertTextFormat !== SNIPPET_FORMAT) {
247
- return { range, text: raw, stops: [] };
237
+ const adjusted = item.insertTextMode === 2
238
+ ? indentSnippet({ text: raw, stops: [] }, options.indentationBefore ?? lineBefore.slice(0, range.start.character))
239
+ : { text: raw, stops: [] };
240
+ return { range, text: adjusted.text, stops: [], ...(adjusted.limited ? { limited: true } : {}) };
248
241
  }
249
242
  const followed = range.end.line === caret.line && lineAfter.slice(Math.max(0, range.end.character - caret.character)).startsWith('(');
250
- const parsed = parseSnippet(followed && isCallableKind(item.kind) ? withoutCall(raw) : raw);
251
- return { range, text: parsed.text, stops: tabOrder(parsed) };
243
+ const source = followed && isCallableKind(item.kind) ? withoutCall(raw) : raw;
244
+ const template = options.template?.source === source ? options.template : compileSnippet(source);
245
+ const rendered = template.render(options.resolve, options.variableTransforms);
246
+ const parsed = item.insertTextMode === 2 ? indentSnippet(rendered, options.indentationBefore ?? lineBefore.slice(0, range.start.character)) : rendered;
247
+ return {
248
+ range,
249
+ text: parsed.text,
250
+ stops: tabOrder(parsed),
251
+ fields: parsed.fields,
252
+ ...(options.variableTransforms !== undefined || rendered.transforms === undefined
253
+ ? {}
254
+ : {
255
+ variableTransforms: rendered.transforms.map((range) => ({ ...range.transform, value: rendered.text.slice(range.start, range.end) }))
256
+ }),
257
+ ...(parsed.limited ? { limited: true } : {})
258
+ };
252
259
  }
253
260
  /*
254
261
  * The same insertion at every other caret that has the word the primary caret has before it, so a name typed at
255
262
  * several carets is completed at all of them. A caret that is inside a selection, or after other text, is left alone.
256
263
  */
257
- export function mirroredInsertions(carets, primary, caret, text, insertion) {
264
+ export function mirroredInsertions(carets, primary, caret, text, insertion, includeSelections = false) {
258
265
  const before = insertion.range.start.line === caret.line ? caret.character - insertion.range.start.character : -1;
259
266
  const after = insertion.range.end.line === caret.line ? insertion.range.end.character - caret.character : -1;
260
267
  if (before < 0 || after < 0) {
@@ -263,11 +270,11 @@ export function mirroredInsertions(carets, primary, caret, text, insertion) {
263
270
  const typed = text({ start: insertion.range.start, end: caret });
264
271
  const replaced = text({ start: caret, end: insertion.range.end });
265
272
  return carets.flatMap((other) => {
266
- const at = other.start;
273
+ const at = includeSelections ? other.end : other.start;
267
274
  if (comparePositions(other.start, primary.start) === 0 && comparePositions(other.end, primary.end) === 0) {
268
275
  return [];
269
276
  }
270
- if (comparePositions(other.start, other.end) !== 0 || at.character < before) {
277
+ if ((!includeSelections && comparePositions(other.start, other.end) !== 0) || at.character < before) {
271
278
  return [];
272
279
  }
273
280
  const range = { start: { line: at.line, character: at.character - before }, end: { line: at.line, character: at.character + after } };
@@ -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>;