@adecore/editor-react 0.39.0 → 0.40.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 (154) hide show
  1. package/dist/CallHierarchyPanel.js +30 -55
  2. package/dist/CodeAuthorsCard.js +0 -1
  3. package/dist/CompletionPopup.js +150 -181
  4. package/dist/DiffView.d.ts +1 -1
  5. package/dist/DiffView.js +2 -29
  6. package/dist/PickPopup.js +54 -80
  7. package/dist/SplitDiffView.js +27 -57
  8. package/dist/SymbolPicker.js +2 -8
  9. package/dist/code-actions-model.js +2 -9
  10. package/dist/code-actions.js +10 -11
  11. package/dist/code-vision.js +2 -1
  12. package/dist/completion-model.js +2 -2
  13. package/dist/completion.js +13 -34
  14. package/dist/definition-link.d.ts +1 -0
  15. package/dist/definition-link.js +22 -18
  16. package/dist/diagnostics-model.d.ts +1 -0
  17. package/dist/diagnostics-model.js +3 -1
  18. package/dist/diagnostics.js +1 -2
  19. package/dist/diff-syntax.d.ts +1 -0
  20. package/dist/diff-syntax.js +31 -0
  21. package/dist/doc-comment-model.d.ts +1 -0
  22. package/dist/doc-comment-model.js +1 -1
  23. package/dist/folding.js +2 -3
  24. package/dist/format-on-type.js +6 -10
  25. package/dist/hover-content.d.ts +0 -2
  26. package/dist/hover-content.js +1 -9
  27. package/dist/inlay-hints.d.ts +0 -1
  28. package/dist/inlay-hints.js +11 -41
  29. package/dist/navigation.js +4 -14
  30. package/dist/peek.js +2 -12
  31. package/dist/popup-focus.d.ts +3 -1
  32. package/dist/popup-focus.js +33 -0
  33. package/dist/recent-choices.d.ts +0 -1
  34. package/dist/recent-choices.js +0 -4
  35. package/dist/rendered-docs.d.ts +1 -0
  36. package/dist/rendered-docs.js +15 -28
  37. package/dist/snippet-field-transforms.d.ts +5 -0
  38. package/dist/snippet-field-transforms.js +27 -16
  39. package/dist/snippet-session.js +8 -20
  40. package/dist/symbol-links.js +3 -10
  41. package/dist/symbol-picker-model.d.ts +1 -0
  42. package/dist/symbol-picker-model.js +7 -0
  43. package/dist/unique.d.ts +10 -0
  44. package/dist/unique.js +15 -0
  45. package/package.json +9 -9
  46. package/src/AnchoredPopup.tsx +2 -0
  47. package/src/BaselineStatus.tsx +1 -0
  48. package/src/CallHierarchyPanel.tsx +16 -6
  49. package/src/ChangeReview.tsx +7 -0
  50. package/src/CodeAuthorsCard.tsx +2 -5
  51. package/src/CompletionPopup.tsx +21 -15
  52. package/src/DiffView.tsx +53 -30
  53. package/src/EditorContextMenu.tsx +7 -0
  54. package/src/EditorView.tsx +28 -0
  55. package/src/FindReplace.tsx +8 -0
  56. package/src/HoverCard.tsx +8 -0
  57. package/src/HoverSections.tsx +11 -0
  58. package/src/LanguagePopups.tsx +1 -0
  59. package/src/PanelResizeHandle.tsx +8 -0
  60. package/src/PathText.tsx +1 -0
  61. package/src/PeekPanel.tsx +13 -0
  62. package/src/PickPopup.tsx +7 -6
  63. package/src/RenameCard.tsx +13 -0
  64. package/src/RenderedDocs.tsx +11 -0
  65. package/src/SplitDiffView.tsx +91 -56
  66. package/src/SymbolPicker.tsx +10 -18
  67. package/src/call-hierarchy-model.ts +10 -0
  68. package/src/call-hierarchy.ts +82 -0
  69. package/src/code-actions-model.ts +21 -9
  70. package/src/code-actions.ts +83 -7
  71. package/src/code-authors.ts +20 -0
  72. package/src/code-vision-model.ts +37 -0
  73. package/src/code-vision.ts +47 -3
  74. package/src/completion-call.ts +19 -0
  75. package/src/completion-model.ts +58 -2
  76. package/src/completion.ts +156 -30
  77. package/src/conflict-model.ts +4 -0
  78. package/src/context-menu-rows.ts +1 -0
  79. package/src/context-menu.ts +7 -0
  80. package/src/definition-link.ts +42 -18
  81. package/src/diagnostics-model.ts +19 -1
  82. package/src/diagnostics.ts +31 -2
  83. package/src/diff-expansion.ts +9 -0
  84. package/src/diff-overlays.tsx +8 -0
  85. package/src/diff-syntax.ts +54 -0
  86. package/src/diff-wordmarks.ts +8 -0
  87. package/src/doc-comment-model.ts +51 -1
  88. package/src/editor-language.ts +11 -0
  89. package/src/find-query.ts +7 -0
  90. package/src/folding.ts +10 -3
  91. package/src/format-on-type.ts +23 -10
  92. package/src/highlight-layers.ts +3 -0
  93. package/src/highlights.ts +18 -0
  94. package/src/history.ts +11 -0
  95. package/src/holder.ts +3 -0
  96. package/src/hover-content.ts +34 -17
  97. package/src/hover.ts +57 -0
  98. package/src/index.ts +4 -0
  99. package/src/inlay-hints.ts +86 -47
  100. package/src/key-priority.ts +2 -0
  101. package/src/line-edits.ts +8 -0
  102. package/src/models.ts +6 -0
  103. package/src/navigation-history.ts +5 -0
  104. package/src/navigation.ts +28 -13
  105. package/src/panel-size.ts +7 -0
  106. package/src/patch-decorations.ts +39 -0
  107. package/src/patch-layout.ts +47 -0
  108. package/src/peek-model.ts +21 -0
  109. package/src/peek.ts +49 -13
  110. package/src/pick.ts +35 -0
  111. package/src/popup-focus.ts +11 -1
  112. package/src/popup-placement.ts +5 -0
  113. package/src/popups.ts +2 -6
  114. package/src/project-language.ts +33 -0
  115. package/src/project-problems.ts +11 -0
  116. package/src/proposal-model.ts +40 -0
  117. package/src/recent-choices.ts +3 -5
  118. package/src/refresher.ts +3 -0
  119. package/src/rename-model.ts +28 -0
  120. package/src/rename.ts +56 -0
  121. package/src/rendered-docs.ts +89 -19
  122. package/src/review-group.ts +4 -0
  123. package/src/row-host.ts +12 -0
  124. package/src/selection-ranges.ts +6 -0
  125. package/src/semantic-delta.ts +7 -0
  126. package/src/semantic-model.ts +11 -0
  127. package/src/semantic-tokens.ts +37 -0
  128. package/src/shortcut-keys.ts +3 -0
  129. package/src/signature-model.ts +7 -0
  130. package/src/signature.ts +27 -0
  131. package/src/snippet-field-transforms.ts +48 -13
  132. package/src/snippet-indent.ts +10 -0
  133. package/src/snippet-range-order.ts +5 -0
  134. package/src/snippet-session.ts +173 -31
  135. package/src/snippet-transform-client.ts +14 -0
  136. package/src/snippet-transform-format.ts +98 -0
  137. package/src/snippet-transform-protocol.ts +5 -0
  138. package/src/snippet-transform-queue.ts +53 -0
  139. package/src/snippet-transform-worker.ts +14 -0
  140. package/src/snippet-variables.ts +9 -0
  141. package/src/snippet.ts +107 -0
  142. package/src/split-alignment.ts +10 -0
  143. package/src/split-scroll.ts +8 -0
  144. package/src/symbol-links.ts +9 -12
  145. package/src/symbol-picker-model.ts +25 -0
  146. package/src/symbol-picker.ts +9 -0
  147. package/src/symbols.ts +21 -0
  148. package/src/testing.ts +7 -0
  149. package/src/timers.ts +5 -0
  150. package/src/unique.ts +23 -0
  151. package/src/use-problem-counts.ts +4 -0
  152. package/src/view-state.ts +9 -0
  153. package/src/word-diff.ts +5 -0
  154. package/src/workspace-edit.ts +84 -0
@@ -6,15 +6,13 @@ import { realTimers, type Timers } from './timers.ts';
6
6
  import { KEY_PRIORITY } from './key-priority.ts';
7
7
  import { MAX_TRANSFORM_CHARACTERS, type SnippetTransformJob } from './snippet-transform-protocol.ts';
8
8
  import type { EditorContentChange, EditorPosition, EditorRange, EditorTextChange } from '@adecore/editor';
9
- import { comparePositions, rangeHolds, shiftPosition } from './diagnostics-model.ts';
9
+ import { comparePositions, endOfInsertion, rangeHolds, sameRange, shiftPosition } from './diagnostics-model.ts';
10
10
  import type { EditorLanguage } from './editor-language.ts';
11
+ import { textBeforeOf } from './line-text.ts';
11
12
  import type { SnippetField, SnippetStop, SnippetTransform } from './snippet.ts';
12
13
 
13
14
  export function positionInInsertion(start: EditorPosition, text: string, offset: number): EditorPosition {
14
- const lines = text.slice(0, offset).split(/\r\n|\r|\n/);
15
- return lines.length === 1
16
- ? { line: start.line, character: start.character + offset }
17
- : { line: start.line + lines.length - 1, character: lines[lines.length - 1]!.length };
15
+ return endOfInsertion(start, text.slice(0, offset));
18
16
  }
19
17
 
20
18
  interface SessionRange extends EditorRange {
@@ -32,6 +30,7 @@ interface Field {
32
30
  choices?: readonly string[];
33
31
  divergent?: boolean;
34
32
  }
33
+
35
34
  interface Instance {
36
35
  fields: Field[];
37
36
  hull: EditorRange;
@@ -44,6 +43,7 @@ function mapRange<Range extends EditorRange>(
44
43
  grow: boolean | ((change: EditorContentChange) => boolean)
45
44
  ): Range {
46
45
  let { start, end } = range;
46
+
47
47
  for (const change of changes) {
48
48
  const grows = typeof grow === 'boolean' ? grow : grow(change);
49
49
  const empty = comparePositions(start, end) === 0;
@@ -52,16 +52,19 @@ function mapRange<Range extends EditorRange>(
52
52
  start = grows && comparePositions(start, change.range.start) === 0 ? change.range.start : shiftPosition(start, change);
53
53
  end = !grows && !empty && insertAtEnd ? end : shiftPosition(end, change);
54
54
  }
55
+
55
56
  return { ...range, start, end };
56
57
  }
57
58
 
58
59
  function selectionsOf(field: Field): SessionRange[] {
59
60
  const primary = field.ranges[field.primary]!;
60
61
  const sorted = [...field.ranges].sort((left, right) => comparePositions(left.start, right.start));
62
+
61
63
  // The editor merges touching selections; keep these mirrors static instead of editing their union.
62
64
  if (sorted.some((range, index) => index > 0 && comparePositions(sorted[index - 1]!.end, range.start) >= 0)) {
63
65
  return [primary];
64
66
  }
67
+
65
68
  return [...field.ranges.filter((range) => range !== primary), primary];
66
69
  }
67
70
 
@@ -81,6 +84,7 @@ export interface SnippetInsertion {
81
84
  }
82
85
 
83
86
  type SnippetHost = Pick<EditorLanguage, 'editor' | 'onDispose'> & Partial<Pick<EditorLanguage, 'popups' | 'pick' | 'project' | 'onKey'>>;
87
+
84
88
  const MAX_FRAMES = 8;
85
89
  const MAX_RANGES = 1000;
86
90
 
@@ -88,9 +92,11 @@ function frameOf(insertions: readonly SnippetInsertion[]): Frame | null {
88
92
  const primary = insertions.at(-1);
89
93
  const positions = [...insertions].sort((left, right) => comparePositions(left.start, right.start));
90
94
  const instanceOrder = new Map(positions.map((insertion, index) => [insertion.start, index]));
95
+
91
96
  if (primary === undefined || primary.stops.length === 0) {
92
97
  return null;
93
98
  }
99
+
94
100
  return {
95
101
  order: primary.stops.map((stop) => stop.index),
96
102
  current: 0,
@@ -101,6 +107,7 @@ function frameOf(insertions: readonly SnippetInsertion[]): Frame | null {
101
107
  selected: [],
102
108
  fields: stops.map((stop): Field => {
103
109
  const field = fields?.find((field) => field.index === stop.index);
110
+
104
111
  return {
105
112
  index: stop.index,
106
113
  primary: field?.primary ?? 0,
@@ -125,41 +132,54 @@ function rangeCount(frame: Frame): number {
125
132
 
126
133
  function selectedOf(frame: Frame): SessionRange[] {
127
134
  const stop = frame.order[frame.current]!;
135
+
128
136
  const selections = frame.instances.flatMap((instance) => {
129
137
  const field = instance.fields.find((field) => field.index === stop);
130
138
  instance.selected = field === undefined ? [] : stop === 0 ? [field.ranges[field.primary]!] : selectionsOf(field);
139
+
131
140
  return instance.selected;
132
141
  });
142
+
133
143
  const sorted = [...selections].sort((left, right) => comparePositions(left.start, right.start));
144
+
134
145
  if (sorted.some((range, index) => index > 0 && comparePositions(sorted[index - 1]!.end, range.start) >= 0)) {
135
146
  const primary = selections.at(-1)!;
147
+
136
148
  for (const instance of frame.instances) {
137
149
  instance.selected = instance.selected.includes(primary) ? [primary] : [];
138
150
  }
151
+
139
152
  return [primary];
140
153
  }
154
+
141
155
  return selections;
142
156
  }
143
157
 
144
158
  function activeOwners(active: readonly SessionRange[], changes: readonly EditorContentChange[]): ReadonlyMap<EditorContentChange, SessionRange> {
145
159
  const sorted = [...active].sort((left, right) => comparePositions(left.start, right.start) || comparePositions(left.end, right.end));
146
160
  const owners = new Map<EditorContentChange, SessionRange>();
161
+
147
162
  for (const edit of changes) {
148
163
  let low = 0;
149
164
  let high = sorted.length;
165
+
150
166
  while (low < high) {
151
167
  const middle = (low + high) >>> 1;
168
+
152
169
  if (comparePositions(sorted[middle]!.start, edit.range.start) <= 0) {
153
170
  low = middle + 1;
154
171
  } else {
155
172
  high = middle;
156
173
  }
157
174
  }
175
+
158
176
  const range = sorted[low - 1];
177
+
159
178
  if (range !== undefined && rangeHolds(range, edit.range.start) && rangeHolds(range, edit.range.end)) {
160
179
  owners.set(edit, range);
161
180
  }
162
181
  }
182
+
163
183
  return owners;
164
184
  }
165
185
 
@@ -175,6 +195,7 @@ export class SnippetFeature {
175
195
  private readonly transforms: SnippetTransformLease | null;
176
196
  private readonly fieldTransforms: SnippetFieldTransforms;
177
197
  private exitGeneration = 0;
198
+
178
199
  private detached: {
179
200
  targets: readonly FieldTransformTarget[];
180
201
  ranges: ReadonlyMap<symbol, EditorRange>;
@@ -186,15 +207,19 @@ export class SnippetFeature {
186
207
  this.transforms = language.project === undefined ? null : acquireSnippetTransforms(language.project, timers);
187
208
  const { editor } = language;
188
209
  this.fieldTransforms = new SnippetFieldTransforms(editor, this.transforms, (targets, values) => this.applyFieldTransforms(targets, values));
210
+
189
211
  const key = (event: KeyboardEvent): boolean => this.key(event);
212
+
190
213
  const offs = [
191
214
  language.onKey === undefined ? editor.onKeyDown(key) : language.onKey(key, KEY_PRIORITY.snippets),
192
215
  editor.onTextChange((change) => this.edited(change)),
193
216
  editor.onCaret(() => this.moved())
194
217
  ];
218
+
195
219
  language.onDispose(() => {
196
220
  this.transforms?.dispose();
197
221
  this.end();
222
+
198
223
  for (const off of offs) {
199
224
  off();
200
225
  }
@@ -205,12 +230,14 @@ export class SnippetFeature {
205
230
  if (this.transforms === null) {
206
231
  return null;
207
232
  }
233
+
208
234
  try {
209
235
  return await this.transforms.request(jobs, { current });
210
236
  } catch (error) {
211
237
  if (error instanceof SnippetTransformError && ['timeout', 'unavailable', 'superseded', 'disposed'].includes(error.kind)) {
212
238
  return null;
213
239
  }
240
+
214
241
  throw error;
215
242
  }
216
243
  }
@@ -225,6 +252,7 @@ export class SnippetFeature {
225
252
  ): Promise<boolean> {
226
253
  let selections = this.language.editor.getSelections(),
227
254
  valid = true;
255
+
228
256
  const off = this.language.editor.onTextChange((change) => {
229
257
  if (this.detached === null) {
230
258
  valid = false;
@@ -233,20 +261,16 @@ export class SnippetFeature {
233
261
  edited?.(change.changes, this.detached.map);
234
262
  }
235
263
  });
264
+
236
265
  try {
237
266
  await this.fieldTransforms.settle();
238
267
  } finally {
239
268
  off();
240
269
  }
270
+
241
271
  const now = this.language.editor.getSelections();
242
- return (
243
- valid &&
244
- current() &&
245
- now.length === selections.length &&
246
- now.every(
247
- (range, index) => comparePositions(range.start, selections[index]!.start) === 0 && comparePositions(range.end, selections[index]!.end) === 0
248
- )
249
- );
272
+
273
+ return valid && current() && now.length === selections.length && now.every((range, index) => sameRange(range, selections[index]!));
250
274
  }
251
275
 
252
276
  get isActive(): boolean {
@@ -271,18 +295,25 @@ export class SnippetFeature {
271
295
  : insertion.fields.reduce((count, field) => count + field.ranges.length + (field.linked?.length ?? 0), 1)),
272
296
  0
273
297
  );
298
+
274
299
  if (ranges > MAX_RANGES) {
275
300
  this.end();
301
+
276
302
  return;
277
303
  }
304
+
278
305
  const frame = frameOf(insertions);
306
+
279
307
  if (frame === null) {
280
308
  if (!options.nested) {
281
309
  this.end();
282
310
  }
311
+
283
312
  return;
284
313
  }
314
+
285
315
  const parent = this.frames.at(-1);
316
+
286
317
  const nested =
287
318
  options.nested &&
288
319
  parent !== undefined &&
@@ -291,13 +322,17 @@ export class SnippetFeature {
291
322
  parent.selected.some((range) => rangeHolds(range, instance.hull.start) && rangeHolds(range, instance.hull.end))
292
323
  )
293
324
  );
325
+
294
326
  if (nested && parent !== undefined) {
295
327
  const selectedParents = new Set(this.frames.flatMap((frame) => frame.instances.flatMap((parent) => parent.selected.map((range) => range.id))));
328
+
296
329
  for (const instance of frame.instances) {
297
330
  const enclosing = parent.instances
298
331
  .flatMap((parent) => parent.selected)
299
332
  .find((range) => rangeHolds(range, instance.hull.start) && rangeHolds(range, instance.hull.end));
333
+
300
334
  const emptyHull = comparePositions(instance.hull.start, instance.hull.end) === 0;
335
+
301
336
  const ancestors = this.frames
302
337
  .flatMap((frame) => frame.instances.flatMap((parent) => parent.fields.flatMap((field) => field.ranges)))
303
338
  .filter(
@@ -310,17 +345,21 @@ export class SnippetFeature {
310
345
  (comparePositions(range.start, instance.hull.start) < 0 && comparePositions(instance.hull.end, range.end) < 0))
311
346
  )
312
347
  .map((range) => range.id);
348
+
313
349
  for (const field of instance.fields) {
314
350
  field.ranges = field.ranges.map((range) => ({ ...range, order: [...(enclosing?.order ?? []), ...range.order], ancestors }));
315
351
  }
316
352
  }
317
353
  }
354
+
318
355
  if (!nested || this.frames.length >= MAX_FRAMES || this.frames.reduce((total, frame) => total + rangeCount(frame), rangeCount(frame)) > MAX_RANGES) {
319
356
  this.end();
320
357
  }
358
+
321
359
  if (rangeCount(frame) > MAX_RANGES) {
322
360
  return;
323
361
  }
362
+
324
363
  this.closeChoices();
325
364
  this.frames.push(frame);
326
365
  this.generation++;
@@ -330,6 +369,7 @@ export class SnippetFeature {
330
369
  /* Imports can touch a hull's edge; their affinity must differ from edits inside its active field. */
331
370
  applyEdits(changes: readonly EditorContentChange[], token: number, extras: readonly EditorContentChange[] = []): boolean {
332
371
  const frame = this.frames.at(-1);
372
+
333
373
  const fits =
334
374
  frame !== undefined &&
335
375
  changes.every((edit) =>
@@ -341,12 +381,16 @@ export class SnippetFeature {
341
381
  instance.selected.some((range) => rangeHolds(range, edit.range.start) && rangeHolds(range, edit.range.end))
342
382
  )
343
383
  );
384
+
344
385
  if (token !== this.generation || !fits) {
345
386
  this.end();
387
+
346
388
  return this.language.editor.applyEdits(changes);
347
389
  }
390
+
348
391
  this.applying = true;
349
392
  this.extras = extras;
393
+
350
394
  try {
351
395
  return this.language.editor.applyEdits(changes);
352
396
  } finally {
@@ -366,9 +410,11 @@ export class SnippetFeature {
366
410
 
367
411
  private closeChoices(): void {
368
412
  const view = this.language.popups?.getState().pick;
413
+
369
414
  if (this.choicePrefix !== null && view?.groups.some((group) => group.rows.some((row) => row.id.startsWith(this.choicePrefix!)))) {
370
415
  this.language.pick?.close();
371
416
  }
417
+
372
418
  this.choicePrefix = null;
373
419
  }
374
420
 
@@ -377,6 +423,7 @@ export class SnippetFeature {
377
423
  this.frames.pop();
378
424
  this.generation++;
379
425
  const parent = this.frames.at(-1);
426
+
380
427
  if (parent !== undefined) {
381
428
  selectedOf(parent);
382
429
  parent.expectedSelections = this.language.editor.getSelections().length;
@@ -385,9 +432,11 @@ export class SnippetFeature {
385
432
 
386
433
  private visit(index: number, choices = true): void {
387
434
  const frame = this.frames.at(-1);
435
+
388
436
  if (frame === undefined) {
389
437
  return;
390
438
  }
439
+
391
440
  this.closeChoices();
392
441
  this.generation++;
393
442
  frame.current = index;
@@ -396,20 +445,26 @@ export class SnippetFeature {
396
445
  this.fieldTransforms.prune();
397
446
  const selections = selectedOf(frame);
398
447
  this.placed = selections;
448
+
399
449
  if (selections.length === 0) {
400
450
  this.end();
451
+
401
452
  return;
402
453
  }
454
+
403
455
  this.visiting = true;
456
+
404
457
  try {
405
458
  this.language.editor.setSelections(selections.map(({ start, end }) => ({ start, end })));
406
459
  frame.expectedSelections = this.language.editor.getSelections().length;
407
460
  } finally {
408
461
  this.visiting = false;
409
462
  }
463
+
410
464
  if (choices && stop !== 0) {
411
465
  this.announceField(frame, index);
412
466
  }
467
+
413
468
  if (stop === 0 || index === frame.order.length - 1) {
414
469
  this.pop();
415
470
  } else if (choices) {
@@ -420,12 +475,15 @@ export class SnippetFeature {
420
475
  /* Says which field the caret moved to, with the number of choices when the field is a list. */
421
476
  private announceField(frame: Frame, index: number): void {
422
477
  const i18n = this.language.project?.i18n;
478
+
423
479
  if (i18n === undefined) {
424
480
  return;
425
481
  }
482
+
426
483
  const total = frame.order.filter((stop) => stop !== 0).length;
427
484
  const options = { current: index + 1, total };
428
485
  const choices = this.fieldOf(frame)?.choices?.length ?? 0;
486
+
429
487
  this.language.editor.announce(
430
488
  choices > 0 && index < frame.order.length - 1
431
489
  ? i18n.t('editor:language.announce.fieldChoices', { ...options, count: choices })
@@ -433,18 +491,21 @@ export class SnippetFeature {
433
491
  );
434
492
  }
435
493
 
436
- private fieldOf(frame: Frame): Frame['instances'][number]['fields'][number] | undefined {
494
+ private fieldOf(frame: Frame): Field | undefined {
437
495
  return frame.instances.at(-1)?.fields.find((field) => field.index === frame.order[frame.current]);
438
496
  }
439
497
 
440
498
  private openChoices(frame: Frame, activeChoice?: number): void {
441
499
  const field = this.fieldOf(frame);
500
+
442
501
  if (field?.choices === undefined || field.choices.length === 0 || this.language.pick === undefined) {
443
502
  return;
444
503
  }
504
+
445
505
  const token = this.generation;
446
506
  const prefix = `snippet-${token}-`;
447
507
  this.choicePrefix = prefix;
508
+
448
509
  this.language.pick.open({
449
510
  anchor: field.ranges[field.primary]!.start,
450
511
  title: this.language.project?.i18n.t('editor:language.snippet.choices'),
@@ -454,12 +515,16 @@ export class SnippetFeature {
454
515
  if (token !== this.generation) {
455
516
  return;
456
517
  }
518
+
457
519
  const text = field.choices![Number(id.slice(prefix.length))];
520
+
458
521
  if (text === undefined) {
459
522
  return;
460
523
  }
524
+
461
525
  const changes = frame.instances.flatMap((instance) => instance.selected.map((range) => ({ range, text })));
462
526
  this.applyEdits(changes, token);
527
+
463
528
  if (this.frames.at(-1) === frame) {
464
529
  this.visit(frame.current, false);
465
530
  }
@@ -474,10 +539,13 @@ export class SnippetFeature {
474
539
 
475
540
  private step(backwards: boolean): void {
476
541
  const frame = this.frames.at(-1);
542
+
477
543
  if (frame === undefined) {
478
544
  return;
479
545
  }
546
+
480
547
  this.exitField(frame);
548
+
481
549
  if (backwards && frame.current === 0 && this.frames.length > 1) {
482
550
  this.pop();
483
551
  this.visit(this.frames.at(-1)!.current);
@@ -490,26 +558,34 @@ export class SnippetFeature {
490
558
  if ((!this.isActive && !this.fieldTransforms.pending) || event.ctrlKey || event.metaKey || event.altKey || event.isComposing) {
491
559
  return false;
492
560
  }
561
+
493
562
  if (event.key === 'Tab' && this.isActive) {
494
563
  this.step(event.shiftKey);
564
+
495
565
  return true;
496
566
  }
567
+
497
568
  if (event.key === 'Escape') {
498
569
  const popups = this.language.popups?.getState();
570
+
499
571
  if (
500
572
  popups !== undefined &&
501
573
  [popups.completion, popups.signature, popups.hover, popups.pick, popups.peek, popups.callHierarchy].some((popup) => popup !== null)
502
574
  ) {
503
575
  return false;
504
576
  }
577
+
505
578
  const active = this.isActive;
506
579
  const caret = this.language.editor.getCaret();
507
580
  this.end();
581
+
508
582
  if (active) {
509
583
  this.language.editor.setCaret(caret);
510
584
  }
585
+
511
586
  return true;
512
587
  }
588
+
513
589
  return false;
514
590
  }
515
591
 
@@ -519,12 +595,15 @@ export class SnippetFeature {
519
595
  frame.exits.set(index, seq);
520
596
  const generation = this.exitGeneration;
521
597
  const targets: FieldTransformTarget[] = [];
598
+
522
599
  for (const instance of frame.instances) {
523
600
  const field = instance.fields.find((field) => field.index === index);
601
+
524
602
  for (const range of field?.ranges ?? []) {
525
603
  if (range.transform === undefined) {
526
604
  continue;
527
605
  }
606
+
528
607
  targets.push({
529
608
  id: range.id,
530
609
  range,
@@ -538,6 +617,7 @@ export class SnippetFeature {
538
617
  });
539
618
  }
540
619
  }
620
+
541
621
  if (
542
622
  targets.length === 0 ||
543
623
  this.transforms === null ||
@@ -545,18 +625,23 @@ export class SnippetFeature {
545
625
  ) {
546
626
  return;
547
627
  }
628
+
548
629
  for (const instance of frame.instances) {
549
630
  for (const field of instance.fields) {
550
631
  const primary = field.ranges[field.primary];
632
+
551
633
  field.ranges = field.ranges.filter(
552
634
  (range) =>
553
635
  !range.parents?.includes(index) ||
554
636
  !targets.some((target) => target.owner === instance && rangeHolds(target.range, range.start) && rangeHolds(target.range, range.end))
555
637
  );
638
+
556
639
  field.primary = Math.max(0, field.ranges.indexOf(primary!));
557
640
  }
641
+
558
642
  instance.fields = instance.fields.filter((field) => field.ranges.length > 0);
559
643
  }
644
+
560
645
  frame.order = frame.order.filter((stop) => frame.instances.at(-1)?.fields.some((field) => field.index === stop));
561
646
  frame.current = frame.order.indexOf(index);
562
647
  this.fieldTransforms.enqueue(targets, () => generation === this.exitGeneration && frame.exits.get(index) === seq);
@@ -566,6 +651,7 @@ export class SnippetFeature {
566
651
  const { editor } = this.language;
567
652
  const activeFrame = this.frames.at(-1);
568
653
  const active = activeFrame?.instances.flatMap((instance) => instance.selected) ?? [];
654
+
569
655
  if (
570
656
  targets.some(
571
657
  (target) =>
@@ -575,25 +661,34 @@ export class SnippetFeature {
575
661
  ) {
576
662
  return;
577
663
  }
664
+
578
665
  const groups: { change: EditorContentChange; targets: { target: FieldTransformTarget; text: string }[] }[] = [];
579
666
  let characters = 0;
667
+
580
668
  for (const [index, target] of targets.entries()) {
581
- const before = editor.textInRange({ start: { line: target.range.start.line, character: 0 }, end: target.range.start });
669
+ const before = textBeforeOf(editor, target.range.start);
582
670
  const prefix = /^[\t ]*/.exec(before)?.[0] ?? '';
583
671
  const value = values[index]!;
584
672
  let newlines = 0;
673
+
585
674
  for (let at = value.indexOf('\n'); at !== -1; at = value.indexOf('\n', at + 1)) {
586
675
  newlines++;
587
676
  }
677
+
588
678
  characters += value.length + newlines * prefix.length;
679
+
589
680
  if (characters > MAX_TRANSFORM_CHARACTERS) {
590
681
  return;
591
682
  }
683
+
592
684
  const text = value.replace(/\n/g, '\n' + prefix);
685
+
593
686
  if (text === editor.textInRange(target.range)) {
594
687
  continue;
595
688
  }
689
+
596
690
  const empty = comparePositions(target.range.start, target.range.end) === 0;
691
+
597
692
  const group = empty
598
693
  ? groups.find(
599
694
  (group) =>
@@ -601,6 +696,7 @@ export class SnippetFeature {
601
696
  comparePositions(group.change.range.start, target.range.start) === 0
602
697
  )
603
698
  : undefined;
699
+
604
700
  if (group === undefined) {
605
701
  groups.push({ change: { range: target.range, text }, targets: [{ target, text }] });
606
702
  } else {
@@ -608,25 +704,32 @@ export class SnippetFeature {
608
704
  group.targets.push({ target, text });
609
705
  }
610
706
  }
707
+
611
708
  if (groups.length === 0) {
612
709
  return;
613
710
  }
711
+
614
712
  for (const group of groups) {
615
713
  group.targets.sort((left, right) => compareSnippetOrder(left.target.order, right.target.order));
616
714
  group.change = { range: group.change.range, text: group.targets.map(({ text }) => text).join('') };
617
715
  }
716
+
618
717
  const changes = groups.map((group) => group.change).sort((left, right) => comparePositions(right.range.start, left.range.start));
619
718
  const peersOf = new Map(groups.map((group) => [group.change, group.targets.map(({ target }) => target)]));
620
719
  const ranges = new Map<symbol, EditorRange>();
621
720
  const steps = new Map<symbol, EditorRange>();
721
+
622
722
  for (const group of groups) {
623
723
  let start = group.change.range.start;
724
+
624
725
  for (const edit of changes) {
625
726
  if (comparePositions(edit.range.start, group.change.range.start) < 0) {
626
727
  start = shiftPosition(start, edit);
627
728
  }
628
729
  }
730
+
629
731
  let local = group.change.range.start;
732
+
630
733
  for (const { target, text } of group.targets) {
631
734
  const localEnd = positionInInsertion(local, text, text.length);
632
735
  steps.set(target.id, { start: local, end: localEnd });
@@ -636,12 +739,16 @@ export class SnippetFeature {
636
739
  start = end;
637
740
  }
638
741
  }
742
+
639
743
  const frame = this.frames.at(-1);
640
744
  const pick = this.language.popups?.getState().pick;
745
+
641
746
  const activeChoice =
642
747
  this.choicePrefix !== null && pick?.active.startsWith(this.choicePrefix) ? Number(pick.active.slice(this.choicePrefix.length)) : undefined;
748
+
643
749
  const trackedFrames = [...new Set([...this.frames, ...targets.map((target) => target.frame as Frame)])];
644
750
  const transformIds = new Set(targets.map((target) => target.id));
751
+
645
752
  const captured = [
646
753
  ...this.placed,
647
754
  ...active,
@@ -653,6 +760,7 @@ export class SnippetFeature {
653
760
  )
654
761
  )
655
762
  ];
763
+
656
764
  const ancestorsOf = new Map(
657
765
  targets.map((target) => [
658
766
  target,
@@ -670,36 +778,43 @@ export class SnippetFeature {
670
778
  ])
671
779
  ])
672
780
  );
781
+
673
782
  const map = <Range extends EditorRange>(range: Range, parent?: (target: FieldTransformTarget) => boolean): Range => {
674
- const tracked =
675
- 'order' in range
676
- ? (range as Range & SessionRange)
677
- : captured.find((candidate) => comparePositions(candidate.start, range.start) === 0 && comparePositions(candidate.end, range.end) === 0);
783
+ const tracked = 'order' in range ? (range as Range & SessionRange) : captured.find((candidate) => sameRange(candidate, range));
678
784
  let { start, end } = range;
785
+
679
786
  for (const change of changes) {
680
787
  const peers = peersOf.get(change)!;
681
788
  const descendants = peers.filter((peer) => parent?.(peer) ?? (tracked !== undefined && ancestorsOf.get(peer)!.has(tracked.id)));
682
789
  const grows = descendants.length > 0;
683
790
  const empty = comparePositions(start, end) === 0;
791
+
684
792
  const boundary = (position: EditorPosition): EditorPosition => {
685
793
  if (comparePositions(position, change.range.start) !== 0 || peers.length === 0) {
686
794
  return shiftPosition(position, change);
687
795
  }
796
+
688
797
  let low = 0,
689
798
  high = peers.length;
799
+
690
800
  while (low < high) {
691
801
  const middle = (low + high) >>> 1;
802
+
692
803
  if (compareSnippetOrder(peers[middle]!.order, tracked?.order ?? [Infinity]) < 0) {
693
804
  low = middle + 1;
694
805
  } else {
695
806
  high = middle;
696
807
  }
697
808
  }
809
+
698
810
  return low === 0 ? steps.get(peers[0]!.id)!.start : steps.get(peers[low - 1]!.id)!.end;
699
811
  };
812
+
700
813
  // A grown range takes in only its own descendants' output; a sibling's output at the same point stays outside.
701
814
  const atChange = (position: EditorPosition): boolean => comparePositions(position, change.range.start) === 0;
815
+
702
816
  start = grows && atChange(start) ? steps.get(descendants[0]!.id)!.start : grows ? shiftPosition(start, change) : boundary(start);
817
+
703
818
  end = grows
704
819
  ? atChange(end)
705
820
  ? steps.get(descendants.at(-1)!.id)!.end
@@ -708,16 +823,20 @@ export class SnippetFeature {
708
823
  ? end
709
824
  : boundary(end);
710
825
  }
826
+
711
827
  return { ...range, start, end };
712
828
  };
829
+
713
830
  this.detached = { targets, ranges, map };
714
831
  this.applying = true;
832
+
715
833
  try {
716
834
  editor.applyEdits(changes, { mapSelections: (range) => map(range) });
717
835
  } finally {
718
836
  this.applying = false;
719
837
  this.detached = null;
720
838
  }
839
+
721
840
  // The generic picker closes on any text edit, including an exit elsewhere in the snippet.
722
841
  if (frame !== undefined && frame === this.frames.at(-1) && activeChoice !== undefined && this.language.popups?.getState().pick === null) {
723
842
  this.openChoices(frame, activeChoice);
@@ -727,24 +846,31 @@ export class SnippetFeature {
727
846
  private mapFrame(frame: Frame, changes: readonly EditorContentChange[], extras: ReadonlySet<EditorContentChange>): boolean {
728
847
  const index = frame.order[frame.current]!;
729
848
  const owned = this.detached === null ? changes.filter((edit) => !extras.has(edit)) : [];
849
+
730
850
  for (const instance of frame.instances) {
731
851
  const active = instance.fields.find((field) => field.index === index)?.ranges ?? [];
732
852
  const owners = activeOwners(active, owned);
853
+
733
854
  instance.selected = instance.selected.map((range) =>
734
855
  this.detached === null ? mapRange(range, changes, (edit) => !extras.has(edit)) : this.detached.map(range)
735
856
  );
857
+
736
858
  const values = instance.selected.map((range) => this.language.editor.textInRange(range));
737
859
  const field = instance.fields.find((field) => field.index === index);
860
+
738
861
  if (values.some((value) => value !== values[0])) {
739
862
  if (field === undefined || (!this.applying && !field.divergent)) {
740
863
  return false;
741
864
  }
865
+
742
866
  // Per-caret completion variables intentionally give parent mirrors different values.
743
867
  field.divergent = true;
744
868
  } else if (field !== undefined) {
745
869
  field.divergent = false;
746
870
  }
871
+
747
872
  const rangeIds = new Set(instance.fields.flatMap((field) => field.ranges.map((range) => range.id)));
873
+
748
874
  instance.hull = mapRange(instance.hull, changes, (edit) =>
749
875
  this.detached === null
750
876
  ? !extras.has(edit)
@@ -754,8 +880,10 @@ export class SnippetFeature {
754
880
  comparePositions(target.range.start, edit.range.start) === 0
755
881
  )
756
882
  );
883
+
757
884
  for (const field of instance.fields) {
758
885
  const primary = field.ranges[field.primary];
886
+
759
887
  field.ranges = field.ranges.filter(
760
888
  (range) =>
761
889
  !range.parents?.includes(index) ||
@@ -767,16 +895,18 @@ export class SnippetFeature {
767
895
  : comparePositions(edit.range.start, range.end) < 0 && comparePositions(range.start, edit.range.end) < 0)
768
896
  )
769
897
  );
770
- field.primary = Math.max(
771
- 0,
772
- field.ranges.findIndex((range) => range === primary)
773
- );
898
+
899
+ field.primary = Math.max(0, field.ranges.indexOf(primary!));
900
+
774
901
  field.ranges = field.ranges.map((range) => {
775
902
  const replacement = this.detached?.ranges.get(range.id);
903
+
776
904
  if (replacement !== undefined) {
777
905
  field.divergent = true;
906
+
778
907
  return { ...range, ...replacement };
779
908
  }
909
+
780
910
  if (this.detached !== null) {
781
911
  return this.detached.map(
782
912
  range,
@@ -789,8 +919,10 @@ export class SnippetFeature {
789
919
  rangeHolds(range, target.range.end))
790
920
  );
791
921
  }
922
+
792
923
  return mapRange(range, changes, (edit) => {
793
924
  const owner = owners.get(edit);
925
+
794
926
  return (
795
927
  owner !== undefined &&
796
928
  (field.index === index
@@ -800,7 +932,9 @@ export class SnippetFeature {
800
932
  });
801
933
  });
802
934
  }
935
+
803
936
  instance.fields = instance.fields.filter((field) => field.ranges.length > 0);
937
+
804
938
  if (!instance.fields.some((field) => field.index === 0)) {
805
939
  instance.fields.push({
806
940
  index: 0,
@@ -809,36 +943,37 @@ export class SnippetFeature {
809
943
  });
810
944
  }
811
945
  }
946
+
812
947
  frame.order = frame.order.filter((index) => frame.instances.at(-1)?.fields.some((field) => field.index === index));
813
948
  frame.current = frame.order.indexOf(index);
949
+
814
950
  return frame.current !== -1;
815
951
  }
816
952
 
817
953
  private edited(change: EditorTextChange): void {
818
954
  if (change.source !== 'input' && !this.applying) {
819
955
  this.end();
956
+
820
957
  return;
821
958
  }
959
+
822
960
  this.fieldTransforms.edited(change.changes, this.detached?.map);
823
961
  this.placed = this.placed.map((range) => (this.detached === null ? mapRange(range, change.changes, true) : this.detached.map(range)));
962
+
824
963
  if (!this.isActive) {
825
964
  return;
826
965
  }
966
+
827
967
  const extras = new Set(
828
- change.changes.filter((change) =>
829
- this.extras.some(
830
- (extra) =>
831
- comparePositions(extra.range.start, change.range.start) === 0 &&
832
- comparePositions(extra.range.end, change.range.end) === 0 &&
833
- extra.text === change.text
834
- )
835
- )
968
+ change.changes.filter((change) => this.extras.some((extra) => sameRange(extra.range, change.range) && extra.text === change.text))
836
969
  );
970
+
837
971
  for (const frame of this.frames) {
838
972
  if (!this.mapFrame(frame, change.changes, extras)) {
839
973
  const caret = this.language.editor.getCaret();
840
974
  this.end();
841
975
  this.language.editor.setCaret(caret);
976
+
842
977
  return;
843
978
  }
844
979
  }
@@ -846,20 +981,27 @@ export class SnippetFeature {
846
981
 
847
982
  private moved(): void {
848
983
  const frame = this.frames.at(-1);
984
+
849
985
  if (frame === undefined || this.visiting || this.applying) {
850
986
  return;
851
987
  }
988
+
852
989
  const { editor } = this.language;
990
+
853
991
  if (editor.getSelections().length < frame.expectedSelections) {
854
992
  this.end();
993
+
855
994
  return;
856
995
  }
996
+
857
997
  const generation = this.generation;
998
+
858
999
  // The caret event precedes text changes; wait until ranges have followed the edit.
859
1000
  queueMicrotask(() => {
860
1001
  if (generation !== this.generation) {
861
1002
  return;
862
1003
  }
1004
+
863
1005
  while (this.frames.length > 0 && !rangeHolds(this.frames.at(-1)!.instances.at(-1)!.hull, editor.getCaret())) {
864
1006
  this.pop();
865
1007
  }