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