@adecore/editor-react 0.22.0 → 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
@@ -1,109 +1,706 @@
1
- import { comparePositions, rangeHolds, shiftRange } from './diagnostics-model.js';
2
- /* Where an offset of inserted text lands when the text is put in at `start`. */
1
+ import { compareSnippetOrder, snippetOrderOf } from './snippet-range-order.js';
2
+ import { SnippetFieldTransforms } from './snippet-field-transforms.js';
3
+ import { SnippetTransformError } from './snippet-transform-client.js';
4
+ import { acquireSnippetTransforms } from './snippet-transform-queue.js';
5
+ import { realTimers } from './timers.js';
6
+ import { MAX_TRANSFORM_CHARACTERS } from './snippet-transform-protocol.js';
7
+ import { comparePositions, rangeHolds, shiftPosition } from './diagnostics-model.js';
3
8
  export function positionInInsertion(start, text, offset) {
4
9
  const lines = text.slice(0, offset).split(/\r\n|\r|\n/);
5
10
  return lines.length === 1
6
11
  ? { line: start.line, character: start.character + offset }
7
12
  : { line: start.line + lines.length - 1, character: lines[lines.length - 1].length };
8
13
  }
9
- /*
10
- * The tab stops of a snippet that was just inserted. Tab and Shift+Tab move between them; the last stop,
11
- * Escape, a caret that leaves the snippet and any change that is not typing end it. A mirror of a stop
12
- * follows the text but is not edited along.
13
- */
14
+ function mapRange(range, changes, grow) {
15
+ let { start, end } = range;
16
+ for (const change of changes) {
17
+ const grows = typeof grow === 'boolean' ? grow : grow(change);
18
+ const empty = comparePositions(start, end) === 0;
19
+ const insertAtEnd = comparePositions(change.range.start, change.range.end) === 0 && comparePositions(end, change.range.start) === 0;
20
+ // Active fields and their parents include text inserted at their left edge.
21
+ start = grows && comparePositions(start, change.range.start) === 0 ? change.range.start : shiftPosition(start, change);
22
+ end = !grows && !empty && insertAtEnd ? end : shiftPosition(end, change);
23
+ }
24
+ return { ...range, start, end };
25
+ }
26
+ function selectionsOf(field) {
27
+ const primary = field.ranges[field.primary];
28
+ const sorted = [...field.ranges].sort((left, right) => comparePositions(left.start, right.start));
29
+ // The editor merges touching selections; keep these mirrors static instead of editing their union.
30
+ if (sorted.some((range, index) => index > 0 && comparePositions(sorted[index - 1].end, range.start) >= 0)) {
31
+ return [primary];
32
+ }
33
+ return [...field.ranges.filter((range) => range !== primary), primary];
34
+ }
35
+ const MAX_FRAMES = 8;
36
+ const MAX_RANGES = 1000;
37
+ function frameOf(insertions) {
38
+ const primary = insertions.at(-1);
39
+ const positions = [...insertions].sort((left, right) => comparePositions(left.start, right.start));
40
+ const instanceOrder = new Map(positions.map((insertion, index) => [insertion.start, index]));
41
+ if (primary === undefined || primary.stops.length === 0) {
42
+ return null;
43
+ }
44
+ return {
45
+ order: primary.stops.map((stop) => stop.index),
46
+ current: 0,
47
+ expectedSelections: 0,
48
+ exits: new Map(),
49
+ instances: insertions.map(({ start: at, text, stops, fields }) => ({
50
+ hull: { start: at, end: positionInInsertion(at, text, text.length) },
51
+ selected: [],
52
+ fields: stops.map((stop) => {
53
+ const field = fields?.find((field) => field.index === stop.index);
54
+ return {
55
+ index: stop.index,
56
+ primary: field?.primary ?? 0,
57
+ choices: field?.choices,
58
+ ranges: [...(field?.ranges ?? [stop]), ...(field?.linked ?? [])].map((range) => ({
59
+ id: Symbol('snippet-range'),
60
+ order: [instanceOrder.get(at), snippetOrderOf(range) ?? (stop.index === 0 ? Infinity : stops.indexOf(stop))],
61
+ ...(range.transform === undefined ? {} : { transform: range.transform }),
62
+ start: positionInInsertion(at, text, range.start),
63
+ end: positionInInsertion(at, text, range.end),
64
+ ...(range.parents === undefined ? {} : { parents: range.parents })
65
+ }))
66
+ };
67
+ })
68
+ }))
69
+ };
70
+ }
71
+ function rangeCount(frame) {
72
+ return frame.instances.reduce((total, instance) => total + instance.fields.reduce((count, field) => count + field.ranges.length, 0), 0);
73
+ }
74
+ function selectedOf(frame) {
75
+ const stop = frame.order[frame.current];
76
+ const selections = frame.instances.flatMap((instance) => {
77
+ const field = instance.fields.find((field) => field.index === stop);
78
+ instance.selected = field === undefined ? [] : stop === 0 ? [field.ranges[field.primary]] : selectionsOf(field);
79
+ return instance.selected;
80
+ });
81
+ const sorted = [...selections].sort((left, right) => comparePositions(left.start, right.start));
82
+ if (sorted.some((range, index) => index > 0 && comparePositions(sorted[index - 1].end, range.start) >= 0)) {
83
+ const primary = selections.at(-1);
84
+ for (const instance of frame.instances) {
85
+ instance.selected = instance.selected.includes(primary) ? [primary] : [];
86
+ }
87
+ return [primary];
88
+ }
89
+ return selections;
90
+ }
91
+ function activeOwners(active, changes) {
92
+ const sorted = [...active].sort((left, right) => comparePositions(left.start, right.start) || comparePositions(left.end, right.end));
93
+ const owners = new Map();
94
+ for (const edit of changes) {
95
+ let low = 0;
96
+ let high = sorted.length;
97
+ while (low < high) {
98
+ const middle = (low + high) >>> 1;
99
+ if (comparePositions(sorted[middle].start, edit.range.start) <= 0) {
100
+ low = middle + 1;
101
+ }
102
+ else {
103
+ high = middle;
104
+ }
105
+ }
106
+ const range = sorted[low - 1];
107
+ if (range !== undefined && rangeHolds(range, edit.range.start) && rangeHolds(range, edit.range.end)) {
108
+ owners.set(edit, range);
109
+ }
110
+ }
111
+ return owners;
112
+ }
14
113
  export class SnippetFeature {
15
114
  language;
16
- stops = [];
17
- current = 0;
18
- constructor(language) {
115
+ frames = [];
116
+ placed = [];
117
+ visiting = false;
118
+ applying = false;
119
+ extras = [];
120
+ generation = 0;
121
+ choicePrefix = null;
122
+ transforms;
123
+ fieldTransforms;
124
+ exitGeneration = 0;
125
+ detached = null;
126
+ constructor(language, timers = realTimers) {
19
127
  this.language = language;
128
+ this.transforms = language.project === undefined ? null : acquireSnippetTransforms(language.project, timers);
20
129
  const { editor } = language;
21
- const offs = [
22
- editor.onKeyDown((event) => this.key(event)),
23
- editor.onTextChange((change) => this.edited(change)),
24
- editor.onCaret((position) => this.moved(position))
25
- ];
130
+ this.fieldTransforms = new SnippetFieldTransforms(editor, this.transforms, (targets, values) => this.applyFieldTransforms(targets, values));
131
+ const offs = [editor.onKeyDown((event) => this.key(event)), editor.onTextChange((change) => this.edited(change)), editor.onCaret(() => this.moved())];
26
132
  language.onDispose(() => {
133
+ this.transforms?.dispose();
134
+ this.end();
27
135
  for (const off of offs) {
28
136
  off();
29
137
  }
30
138
  });
31
139
  }
140
+ async transformVariables(jobs, current = () => true) {
141
+ if (this.transforms === null) {
142
+ return null;
143
+ }
144
+ try {
145
+ return await this.transforms.request(jobs, { current });
146
+ }
147
+ catch (error) {
148
+ if (error instanceof SnippetTransformError && ['timeout', 'unavailable', 'superseded', 'disposed'].includes(error.kind)) {
149
+ return null;
150
+ }
151
+ throw error;
152
+ }
153
+ }
154
+ get hasPendingTransforms() {
155
+ return this.fieldTransforms.pending;
156
+ }
157
+ async settleTransforms(current, edited) {
158
+ let selections = this.language.editor.getSelections(), valid = true;
159
+ const off = this.language.editor.onTextChange((change) => {
160
+ if (this.detached === null) {
161
+ valid = false;
162
+ }
163
+ else {
164
+ selections = selections.map((range) => this.detached.map(range));
165
+ edited?.(change.changes, this.detached.map);
166
+ }
167
+ });
168
+ try {
169
+ await this.fieldTransforms.settle();
170
+ }
171
+ finally {
172
+ off();
173
+ }
174
+ const now = this.language.editor.getSelections();
175
+ return (valid &&
176
+ current() &&
177
+ now.length === selections.length &&
178
+ now.every((range, index) => comparePositions(range.start, selections[index].start) === 0 && comparePositions(range.end, selections[index].end) === 0));
179
+ }
32
180
  get isActive() {
33
- return this.stops.length > 0;
181
+ return this.frames.length > 0;
34
182
  }
35
- /* Puts the caret where the snippet says, and keeps the stops to go through when there is more than the last one. */
36
- begin(start, text, stops) {
37
- this.end();
38
- if (stops.length === 0) {
183
+ get token() {
184
+ return this.generation;
185
+ }
186
+ begin(start, text, stops, fields) {
187
+ const starts = Array.isArray(start) ? start : [start];
188
+ this.beginInstances(starts.map((start) => ({ start, text, stops, fields })));
189
+ }
190
+ beginInstances(insertions, options = {}) {
191
+ const ranges = insertions.reduce((total, insertion) => total +
192
+ (insertion.fields === undefined
193
+ ? insertion.stops.length
194
+ : insertion.fields.reduce((count, field) => count + field.ranges.length + (field.linked?.length ?? 0), 1)), 0);
195
+ if (ranges > MAX_RANGES) {
196
+ this.end();
39
197
  return;
40
198
  }
41
- const ranges = stops.map((stop) => ({ start: positionInInsertion(start, text, stop.start), end: positionInInsertion(start, text, stop.end) }));
42
- if (ranges.length === 1) {
43
- this.language.editor.setCaret(ranges[0].start);
199
+ const frame = frameOf(insertions);
200
+ if (frame === null) {
201
+ if (!options.nested) {
202
+ this.end();
203
+ }
44
204
  return;
45
205
  }
46
- this.stops = ranges;
206
+ const parent = this.frames.at(-1);
207
+ const nested = options.nested &&
208
+ parent !== undefined &&
209
+ frame.instances.every((instance) => parent.instances.some((parent) => parent.selected.some((range) => rangeHolds(range, instance.hull.start) && rangeHolds(range, instance.hull.end))));
210
+ if (nested && parent !== undefined) {
211
+ const selectedParents = new Set(this.frames.flatMap((frame) => frame.instances.flatMap((parent) => parent.selected.map((range) => range.id))));
212
+ for (const instance of frame.instances) {
213
+ const enclosing = parent.instances
214
+ .flatMap((parent) => parent.selected)
215
+ .find((range) => rangeHolds(range, instance.hull.start) && rangeHolds(range, instance.hull.end));
216
+ const emptyHull = comparePositions(instance.hull.start, instance.hull.end) === 0;
217
+ const ancestors = this.frames
218
+ .flatMap((frame) => frame.instances.flatMap((parent) => parent.fields.flatMap((field) => field.ranges)))
219
+ .filter((range) => (comparePositions(range.start, range.end) < 0 || selectedParents.has(range.id)) &&
220
+ rangeHolds(range, instance.hull.start) &&
221
+ rangeHolds(range, instance.hull.end) &&
222
+ (!emptyHull ||
223
+ range.id === enclosing?.id ||
224
+ (comparePositions(range.start, instance.hull.start) < 0 && comparePositions(instance.hull.end, range.end) < 0)))
225
+ .map((range) => range.id);
226
+ for (const field of instance.fields) {
227
+ field.ranges = field.ranges.map((range) => ({ ...range, order: [...(enclosing?.order ?? []), ...range.order], ancestors }));
228
+ }
229
+ }
230
+ }
231
+ if (!nested || this.frames.length >= MAX_FRAMES || this.frames.reduce((total, frame) => total + rangeCount(frame), rangeCount(frame)) > MAX_RANGES) {
232
+ this.end();
233
+ }
234
+ if (rangeCount(frame) > MAX_RANGES) {
235
+ return;
236
+ }
237
+ this.closeChoices();
238
+ this.frames.push(frame);
239
+ this.generation++;
47
240
  this.visit(0);
48
241
  }
242
+ /* Imports can touch a hull's edge; their affinity must differ from edits inside its active field. */
243
+ applyEdits(changes, token, extras = []) {
244
+ const frame = this.frames.at(-1);
245
+ const fits = frame !== undefined &&
246
+ changes.every((edit) => extras.includes(edit)
247
+ ? frame.instances.every((instance) => comparePositions(edit.range.end, instance.hull.start) <= 0 || comparePositions(edit.range.start, instance.hull.end) >= 0)
248
+ : frame.instances.some((instance) => instance.selected.some((range) => rangeHolds(range, edit.range.start) && rangeHolds(range, edit.range.end))));
249
+ if (token !== this.generation || !fits) {
250
+ this.end();
251
+ return this.language.editor.applyEdits(changes);
252
+ }
253
+ this.applying = true;
254
+ this.extras = extras;
255
+ try {
256
+ return this.language.editor.applyEdits(changes);
257
+ }
258
+ finally {
259
+ this.applying = false;
260
+ this.extras = [];
261
+ }
262
+ }
49
263
  end() {
50
- this.stops = [];
51
- this.current = 0;
264
+ this.exitGeneration++;
265
+ this.fieldTransforms.clear();
266
+ this.closeChoices();
267
+ this.generation++;
268
+ this.frames = [];
269
+ this.placed = [];
52
270
  }
53
- visit(index) {
54
- const { editor } = this.language;
55
- const target = this.stops[index];
56
- this.current = index;
57
- if (comparePositions(target.start, target.end) === 0) {
58
- editor.setCaret(target.start);
271
+ closeChoices() {
272
+ const view = this.language.popups?.getState().pick;
273
+ if (this.choicePrefix !== null && view?.groups.some((group) => group.rows.some((row) => row.id.startsWith(this.choicePrefix)))) {
274
+ this.language.pick?.close();
59
275
  }
60
- else {
61
- editor.setSelection(target);
276
+ this.choicePrefix = null;
277
+ }
278
+ pop() {
279
+ this.closeChoices();
280
+ this.frames.pop();
281
+ this.generation++;
282
+ const parent = this.frames.at(-1);
283
+ if (parent !== undefined) {
284
+ selectedOf(parent);
285
+ parent.expectedSelections = this.language.editor.getSelections().length;
286
+ }
287
+ }
288
+ visit(index, choices = true) {
289
+ const frame = this.frames.at(-1);
290
+ if (frame === undefined) {
291
+ return;
62
292
  }
63
- if (index === this.stops.length - 1) {
293
+ this.closeChoices();
294
+ this.generation++;
295
+ frame.current = index;
296
+ const stop = frame.order[index];
297
+ frame.exits.set(stop, (frame.exits.get(stop) ?? 0) + 1);
298
+ this.fieldTransforms.prune();
299
+ const selections = selectedOf(frame);
300
+ this.placed = selections;
301
+ if (selections.length === 0) {
64
302
  this.end();
303
+ return;
304
+ }
305
+ this.visiting = true;
306
+ try {
307
+ this.language.editor.setSelections(selections.map(({ start, end }) => ({ start, end })));
308
+ frame.expectedSelections = this.language.editor.getSelections().length;
309
+ }
310
+ finally {
311
+ this.visiting = false;
312
+ }
313
+ if (choices && stop !== 0) {
314
+ this.announceField(frame, index);
315
+ }
316
+ if (stop === 0 || index === frame.order.length - 1) {
317
+ this.pop();
318
+ }
319
+ else if (choices) {
320
+ this.openChoices(frame);
321
+ }
322
+ }
323
+ /* Says which field the caret moved to, with the number of choices when the field is a list. */
324
+ announceField(frame, index) {
325
+ const i18n = this.language.project?.i18n;
326
+ if (i18n === undefined) {
327
+ return;
328
+ }
329
+ const total = frame.order.filter((stop) => stop !== 0).length;
330
+ const options = { current: index + 1, total };
331
+ const choices = this.fieldOf(frame)?.choices?.length ?? 0;
332
+ this.language.editor.announce(choices > 0 && index < frame.order.length - 1
333
+ ? i18n.t('editor:language.announce.fieldChoices', { ...options, count: choices })
334
+ : i18n.t('editor:language.announce.field', options));
335
+ }
336
+ fieldOf(frame) {
337
+ return frame.instances.at(-1)?.fields.find((field) => field.index === frame.order[frame.current]);
338
+ }
339
+ openChoices(frame, activeChoice) {
340
+ const field = this.fieldOf(frame);
341
+ if (field?.choices === undefined || field.choices.length === 0 || this.language.pick === undefined) {
342
+ return;
343
+ }
344
+ const token = this.generation;
345
+ const prefix = `snippet-${token}-`;
346
+ this.choicePrefix = prefix;
347
+ this.language.pick.open({
348
+ anchor: field.ranges[field.primary].start,
349
+ title: this.language.project?.i18n.t('editor:language.snippet.choices'),
350
+ active: prefix + (activeChoice ?? Math.max(0, field.choices.indexOf(this.language.editor.textInRange(field.ranges[field.primary])))),
351
+ groups: [{ title: null, rows: field.choices.map((label, index) => ({ id: prefix + index, label, detail: '' })) }],
352
+ accept: (id) => {
353
+ if (token !== this.generation) {
354
+ return;
355
+ }
356
+ const text = field.choices[Number(id.slice(prefix.length))];
357
+ if (text === undefined) {
358
+ return;
359
+ }
360
+ const changes = frame.instances.flatMap((instance) => instance.selected.map((range) => ({ range, text })));
361
+ this.applyEdits(changes, token);
362
+ if (this.frames.at(-1) === frame) {
363
+ this.visit(frame.current, false);
364
+ }
365
+ },
366
+ onTab: (backwards) => {
367
+ if (token === this.generation) {
368
+ this.step(backwards);
369
+ }
370
+ }
371
+ });
372
+ }
373
+ step(backwards) {
374
+ const frame = this.frames.at(-1);
375
+ if (frame === undefined) {
376
+ return;
377
+ }
378
+ this.exitField(frame);
379
+ if (backwards && frame.current === 0 && this.frames.length > 1) {
380
+ this.pop();
381
+ this.visit(this.frames.at(-1).current);
382
+ }
383
+ else {
384
+ this.visit(backwards ? Math.max(0, frame.current - 1) : frame.current + 1);
65
385
  }
66
386
  }
67
387
  key(event) {
68
- if (!this.isActive || event.ctrlKey || event.metaKey || event.altKey) {
388
+ if ((!this.isActive && !this.fieldTransforms.pending) || event.ctrlKey || event.metaKey || event.altKey || event.isComposing) {
69
389
  return false;
70
390
  }
71
- if (event.key === 'Tab') {
72
- this.visit(event.shiftKey ? Math.max(0, this.current - 1) : this.current + 1);
391
+ if (event.key === 'Tab' && this.isActive) {
392
+ this.step(event.shiftKey);
73
393
  return true;
74
394
  }
75
395
  if (event.key === 'Escape') {
396
+ const popups = this.language.popups?.getState();
397
+ if (popups !== undefined &&
398
+ [popups.completion, popups.signature, popups.hover, popups.pick, popups.peek, popups.callHierarchy ?? null].some((popup) => popup !== null)) {
399
+ return false;
400
+ }
401
+ const active = this.isActive;
402
+ const caret = this.language.editor.getCaret();
76
403
  this.end();
404
+ if (active) {
405
+ this.language.editor.setCaret(caret);
406
+ }
77
407
  return true;
78
408
  }
79
409
  return false;
80
410
  }
81
- edited(change) {
82
- if (!this.isActive) {
411
+ exitField(frame) {
412
+ const index = frame.order[frame.current];
413
+ const seq = (frame.exits.get(index) ?? 0) + 1;
414
+ frame.exits.set(index, seq);
415
+ const generation = this.exitGeneration;
416
+ const targets = [];
417
+ for (const instance of frame.instances) {
418
+ const field = instance.fields.find((field) => field.index === index);
419
+ for (const range of field?.ranges ?? []) {
420
+ if (range.transform === undefined) {
421
+ continue;
422
+ }
423
+ targets.push({
424
+ id: range.id,
425
+ range,
426
+ value: this.language.editor.textInRange(range),
427
+ transform: range.transform,
428
+ parents: range.parents,
429
+ owner: instance,
430
+ frame,
431
+ order: range.order,
432
+ ancestors: range.ancestors
433
+ });
434
+ }
435
+ }
436
+ if (targets.length === 0 ||
437
+ this.transforms === null ||
438
+ this.fieldTransforms.ranges + this.frames.reduce((sum, frame) => sum + rangeCount(frame), targets.length) > MAX_RANGES) {
83
439
  return;
84
440
  }
85
- if (change.source !== 'input') {
86
- this.end();
441
+ for (const instance of frame.instances) {
442
+ for (const field of instance.fields) {
443
+ const primary = field.ranges[field.primary];
444
+ field.ranges = field.ranges.filter((range) => !range.parents?.includes(index) ||
445
+ !targets.some((target) => target.owner === instance && rangeHolds(target.range, range.start) && rangeHolds(target.range, range.end)));
446
+ field.primary = Math.max(0, field.ranges.indexOf(primary));
447
+ }
448
+ instance.fields = instance.fields.filter((field) => field.ranges.length > 0);
449
+ }
450
+ frame.order = frame.order.filter((stop) => frame.instances.at(-1)?.fields.some((field) => field.index === stop));
451
+ frame.current = frame.order.indexOf(index);
452
+ this.fieldTransforms.enqueue(targets, () => generation === this.exitGeneration && frame.exits.get(index) === seq);
453
+ }
454
+ applyFieldTransforms(targets, values) {
455
+ const { editor } = this.language;
456
+ const activeFrame = this.frames.at(-1);
457
+ const active = activeFrame?.instances.flatMap((instance) => instance.selected) ?? [];
458
+ if (targets.some((target) => target.frame === activeFrame &&
459
+ active.some((range) => comparePositions(range.start, target.range.end) < 0 && comparePositions(target.range.start, range.end) < 0))) {
87
460
  return;
88
461
  }
89
- this.stops = this.stops.map((stop) => shiftRange(stop, change.changes));
462
+ const groups = [];
463
+ let characters = 0;
464
+ for (const [index, target] of targets.entries()) {
465
+ const before = editor.textInRange({ start: { line: target.range.start.line, character: 0 }, end: target.range.start });
466
+ const prefix = /^[\t ]*/.exec(before)?.[0] ?? '';
467
+ const value = values[index];
468
+ let newlines = 0;
469
+ for (let at = value.indexOf('\n'); at !== -1; at = value.indexOf('\n', at + 1)) {
470
+ newlines++;
471
+ }
472
+ characters += value.length + newlines * prefix.length;
473
+ if (characters > MAX_TRANSFORM_CHARACTERS) {
474
+ return;
475
+ }
476
+ const text = value.replace(/\n/g, '\n' + prefix);
477
+ if (text === editor.textInRange(target.range)) {
478
+ continue;
479
+ }
480
+ const empty = comparePositions(target.range.start, target.range.end) === 0;
481
+ const group = empty
482
+ ? groups.find((group) => comparePositions(group.change.range.start, group.change.range.end) === 0 &&
483
+ comparePositions(group.change.range.start, target.range.start) === 0)
484
+ : undefined;
485
+ if (group === undefined) {
486
+ groups.push({ change: { range: target.range, text }, targets: [{ target, text }] });
487
+ }
488
+ else {
489
+ group.change = { range: group.change.range, text: group.change.text + text };
490
+ group.targets.push({ target, text });
491
+ }
492
+ }
493
+ if (groups.length === 0) {
494
+ return;
495
+ }
496
+ for (const group of groups) {
497
+ group.targets.sort((left, right) => compareSnippetOrder(left.target.order, right.target.order));
498
+ group.change = { range: group.change.range, text: group.targets.map(({ text }) => text).join('') };
499
+ }
500
+ const changes = groups.map((group) => group.change).sort((left, right) => comparePositions(right.range.start, left.range.start));
501
+ const peersOf = new Map(groups.map((group) => [group.change, group.targets.map(({ target }) => target)]));
502
+ const ranges = new Map();
503
+ const steps = new Map();
504
+ for (const group of groups) {
505
+ let start = group.change.range.start;
506
+ for (const edit of changes) {
507
+ if (comparePositions(edit.range.start, group.change.range.start) < 0) {
508
+ start = shiftPosition(start, edit);
509
+ }
510
+ }
511
+ let local = group.change.range.start;
512
+ for (const { target, text } of group.targets) {
513
+ const localEnd = positionInInsertion(local, text, text.length);
514
+ steps.set(target.id, { start: local, end: localEnd });
515
+ local = localEnd;
516
+ const end = positionInInsertion(start, text, text.length);
517
+ ranges.set(target.id, { start, end });
518
+ start = end;
519
+ }
520
+ }
521
+ const frame = this.frames.at(-1);
522
+ const pick = this.language.popups?.getState().pick;
523
+ const activeChoice = this.choicePrefix !== null && pick?.active.startsWith(this.choicePrefix) ? Number(pick.active.slice(this.choicePrefix.length)) : undefined;
524
+ const trackedFrames = [...new Set([...this.frames, ...targets.map((target) => target.frame)])];
525
+ const transformIds = new Set(targets.map((target) => target.id));
526
+ const captured = [
527
+ ...this.placed,
528
+ ...active,
529
+ ...trackedFrames.flatMap((frame) => frame.instances.flatMap((instance) => instance.fields.flatMap((field) => field.ranges.filter((range) => !transformIds.has(range.id) || comparePositions(range.start, range.end) < 0))))
530
+ ];
531
+ const ancestorsOf = new Map(targets.map((target) => [
532
+ target,
533
+ new Set([
534
+ ...(target.ancestors ?? []),
535
+ ...target.frame.instances.flatMap((instance) => instance.fields.flatMap((field) => target.owner === instance && target.parents?.includes(field.index) === true
536
+ ? field.ranges
537
+ .filter((range) => rangeHolds(range, target.range.start) && rangeHolds(range, target.range.end))
538
+ .map((range) => range.id)
539
+ : []))
540
+ ])
541
+ ]));
542
+ const map = (range, parent) => {
543
+ const tracked = 'order' in range
544
+ ? range
545
+ : captured.find((candidate) => comparePositions(candidate.start, range.start) === 0 && comparePositions(candidate.end, range.end) === 0);
546
+ let { start, end } = range;
547
+ for (const change of changes) {
548
+ const peers = peersOf.get(change);
549
+ const descendants = peers.filter((peer) => parent?.(peer) ?? (tracked !== undefined && ancestorsOf.get(peer).has(tracked.id)));
550
+ const grows = descendants.length > 0;
551
+ const empty = comparePositions(start, end) === 0;
552
+ const boundary = (position) => {
553
+ if (comparePositions(position, change.range.start) !== 0 || peers.length === 0) {
554
+ return shiftPosition(position, change);
555
+ }
556
+ let low = 0, high = peers.length;
557
+ while (low < high) {
558
+ const middle = (low + high) >>> 1;
559
+ if (compareSnippetOrder(peers[middle].order, tracked?.order ?? [Infinity]) < 0) {
560
+ low = middle + 1;
561
+ }
562
+ else {
563
+ high = middle;
564
+ }
565
+ }
566
+ return low === 0 ? steps.get(peers[0].id).start : steps.get(peers[low - 1].id).end;
567
+ };
568
+ // A grown range takes in only its own descendants' output; a sibling's output at the same point stays outside.
569
+ const atChange = (position) => comparePositions(position, change.range.start) === 0;
570
+ start = grows && atChange(start) ? steps.get(descendants[0].id).start : grows ? shiftPosition(start, change) : boundary(start);
571
+ end = grows
572
+ ? atChange(end)
573
+ ? steps.get(descendants.at(-1).id).end
574
+ : shiftPosition(end, change)
575
+ : !empty && atChange(end)
576
+ ? end
577
+ : boundary(end);
578
+ }
579
+ return { ...range, start, end };
580
+ };
581
+ this.detached = { targets, ranges, map };
582
+ this.applying = true;
583
+ try {
584
+ editor.applyEdits(changes, { mapSelections: (range) => map(range) });
585
+ }
586
+ finally {
587
+ this.applying = false;
588
+ this.detached = null;
589
+ }
590
+ // The generic picker closes on any text edit, including an exit elsewhere in the snippet.
591
+ if (frame !== undefined && frame === this.frames.at(-1) && activeChoice !== undefined && this.language.popups?.getState().pick === null) {
592
+ this.openChoices(frame, activeChoice);
593
+ }
90
594
  }
91
- /* A caret outside the stretch the stops span is a person moving on. */
92
- moved(position) {
595
+ mapFrame(frame, changes, extras) {
596
+ const index = frame.order[frame.current];
597
+ const owned = this.detached === null ? changes.filter((edit) => !extras.has(edit)) : [];
598
+ for (const instance of frame.instances) {
599
+ const active = instance.fields.find((field) => field.index === index)?.ranges ?? [];
600
+ const owners = activeOwners(active, owned);
601
+ instance.selected = instance.selected.map((range) => this.detached === null ? mapRange(range, changes, (edit) => !extras.has(edit)) : this.detached.map(range));
602
+ const values = instance.selected.map((range) => this.language.editor.textInRange(range));
603
+ const field = instance.fields.find((field) => field.index === index);
604
+ if (values.some((value) => value !== values[0])) {
605
+ if (field === undefined || (!this.applying && !field.divergent)) {
606
+ return false;
607
+ }
608
+ // Per-caret completion variables intentionally give parent mirrors different values.
609
+ field.divergent = true;
610
+ }
611
+ else if (field !== undefined) {
612
+ field.divergent = false;
613
+ }
614
+ const rangeIds = new Set(instance.fields.flatMap((field) => field.ranges.map((range) => range.id)));
615
+ instance.hull = mapRange(instance.hull, changes, (edit) => this.detached === null
616
+ ? !extras.has(edit)
617
+ : this.detached.targets.some((target) => (target.owner === instance || target.ancestors?.some((id) => rangeIds.has(id))) &&
618
+ comparePositions(target.range.start, edit.range.start) === 0));
619
+ for (const field of instance.fields) {
620
+ const primary = field.ranges[field.primary];
621
+ field.ranges = field.ranges.filter((range) => !range.parents?.includes(index) ||
622
+ !owned.some((edit) => comparePositions(edit.range.start, edit.range.end) < 0 &&
623
+ (comparePositions(range.start, range.end) === 0
624
+ ? rangeHolds(edit.range, range.start)
625
+ : comparePositions(edit.range.start, range.end) < 0 && comparePositions(range.start, edit.range.end) < 0)));
626
+ field.primary = Math.max(0, field.ranges.findIndex((range) => range === primary));
627
+ field.ranges = field.ranges.map((range) => {
628
+ const replacement = this.detached?.ranges.get(range.id);
629
+ if (replacement !== undefined) {
630
+ field.divergent = true;
631
+ return { ...range, ...replacement };
632
+ }
633
+ if (this.detached !== null) {
634
+ return this.detached.map(range, (target) => target.ancestors?.includes(range.id) === true ||
635
+ (target.frame === frame &&
636
+ target.owner === instance &&
637
+ target.parents?.includes(field.index) === true &&
638
+ rangeHolds(range, target.range.start) &&
639
+ rangeHolds(range, target.range.end)));
640
+ }
641
+ return mapRange(range, changes, (edit) => {
642
+ const owner = owners.get(edit);
643
+ return (owner !== undefined &&
644
+ (field.index === index
645
+ ? owner === range
646
+ : owner.parents?.includes(field.index) === true && rangeHolds(range, owner.start) && rangeHolds(range, owner.end)));
647
+ });
648
+ });
649
+ }
650
+ instance.fields = instance.fields.filter((field) => field.ranges.length > 0);
651
+ if (!instance.fields.some((field) => field.index === 0)) {
652
+ instance.fields.push({
653
+ index: 0,
654
+ primary: 0,
655
+ ranges: [{ id: Symbol('snippet-final'), order: [Infinity], start: instance.hull.end, end: instance.hull.end }]
656
+ });
657
+ }
658
+ }
659
+ frame.order = frame.order.filter((index) => frame.instances.at(-1)?.fields.some((field) => field.index === index));
660
+ frame.current = frame.order.indexOf(index);
661
+ return frame.current !== -1;
662
+ }
663
+ edited(change) {
664
+ if (change.source !== 'input' && !this.applying) {
665
+ this.end();
666
+ return;
667
+ }
668
+ this.fieldTransforms.edited(change.changes, this.detached?.map);
669
+ this.placed = this.placed.map((range) => (this.detached === null ? mapRange(range, change.changes, true) : this.detached.map(range)));
93
670
  if (!this.isActive) {
94
671
  return;
95
672
  }
96
- const hull = { start: this.stops[0].start, end: this.stops[this.stops.length - 1].end };
97
- for (const stop of this.stops) {
98
- if (comparePositions(stop.start, hull.start) < 0) {
99
- hull.start = stop.start;
100
- }
101
- if (comparePositions(stop.end, hull.end) > 0) {
102
- hull.end = stop.end;
673
+ const extras = new Set(change.changes.filter((change) => this.extras.some((extra) => comparePositions(extra.range.start, change.range.start) === 0 &&
674
+ comparePositions(extra.range.end, change.range.end) === 0 &&
675
+ extra.text === change.text)));
676
+ for (const frame of this.frames) {
677
+ if (!this.mapFrame(frame, change.changes, extras)) {
678
+ const caret = this.language.editor.getCaret();
679
+ this.end();
680
+ this.language.editor.setCaret(caret);
681
+ return;
103
682
  }
104
683
  }
105
- if (!rangeHolds(hull, position)) {
684
+ }
685
+ moved() {
686
+ const frame = this.frames.at(-1);
687
+ if (frame === undefined || this.visiting || this.applying) {
688
+ return;
689
+ }
690
+ const { editor } = this.language;
691
+ if (editor.getSelections().length < frame.expectedSelections) {
106
692
  this.end();
693
+ return;
107
694
  }
695
+ const generation = this.generation;
696
+ // The caret event precedes text changes; wait until ranges have followed the edit.
697
+ queueMicrotask(() => {
698
+ if (generation !== this.generation) {
699
+ return;
700
+ }
701
+ while (this.frames.length > 0 && !rangeHolds(this.frames.at(-1).instances.at(-1).hull, editor.getCaret())) {
702
+ this.pop();
703
+ }
704
+ });
108
705
  }
109
706
  }