@adecore/editor-react 0.22.1 → 0.23.0

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