@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.
- package/dist/CallHierarchyPanel.d.ts +6 -0
- package/dist/CallHierarchyPanel.js +88 -0
- package/dist/CompletionPopup.js +11 -3
- package/dist/DiffView.d.ts +22 -0
- package/dist/DiffView.js +203 -0
- package/dist/EditorContextMenu.js +1 -0
- package/dist/EditorView.js +18 -1
- package/dist/FindReplace.d.ts +1 -1
- package/dist/FindReplace.js +41 -11
- package/dist/HoverCard.js +2 -2
- package/dist/LanguagePopups.js +4 -1
- package/dist/Markdown.d.ts +2 -1
- package/dist/Markdown.js +8 -4
- package/dist/PeekPanel.d.ts +3 -0
- package/dist/PeekPanel.js +2 -3
- package/dist/PickPopup.js +15 -2
- package/dist/RenameCard.js +10 -5
- package/dist/RenderedDocs.d.ts +9 -0
- package/dist/RenderedDocs.js +84 -0
- package/dist/SplitDiffView.d.ts +9 -0
- package/dist/SplitDiffView.js +276 -0
- package/dist/SymbolPicker.d.ts +3 -0
- package/dist/SymbolPicker.js +4 -3
- package/dist/call-hierarchy-model.d.ts +52 -0
- package/dist/call-hierarchy-model.js +99 -0
- package/dist/call-hierarchy.d.ts +42 -0
- package/dist/call-hierarchy.js +399 -0
- package/dist/code-actions-model.d.ts +2 -0
- package/dist/code-actions-model.js +12 -10
- package/dist/code-actions.js +3 -1
- package/dist/completion-model.d.ts +14 -4
- package/dist/completion-model.js +40 -33
- package/dist/completion.d.ts +4 -0
- package/dist/completion.js +257 -42
- package/dist/diagnostics.js +8 -6
- package/dist/diff-expansion.d.ts +9 -0
- package/dist/diff-expansion.js +41 -0
- package/dist/diff-overlays.d.ts +15 -0
- package/dist/diff-overlays.js +28 -0
- package/dist/diff-syntax.d.ts +16 -0
- package/dist/diff-syntax.js +49 -0
- package/dist/diff-wordmarks.d.ts +5 -0
- package/dist/diff-wordmarks.js +26 -0
- package/dist/doc-comment-model.d.ts +23 -0
- package/dist/doc-comment-model.js +291 -0
- package/dist/editor-language.d.ts +13 -1
- package/dist/editor-language.js +14 -4
- package/dist/editor-react.css +79 -0
- package/dist/format-on-type.d.ts +14 -0
- package/dist/format-on-type.js +96 -0
- package/dist/host-types.d.ts +17 -0
- package/dist/hover.d.ts +4 -0
- package/dist/hover.js +15 -2
- package/dist/index.d.ts +19 -5
- package/dist/index.js +9 -1
- package/dist/inlay-hints.d.ts +47 -2
- package/dist/inlay-hints.js +353 -16
- package/dist/language-ids.js +4 -2
- package/dist/locales/en.json +114 -3
- package/dist/locales/nl.json +114 -3
- package/dist/models.d.ts +6 -2
- package/dist/models.js +5 -1
- package/dist/patch-decorations.d.ts +27 -0
- package/dist/patch-decorations.js +182 -0
- package/dist/patch-layout.d.ts +40 -0
- package/dist/patch-layout.js +304 -0
- package/dist/peek.js +1 -0
- package/dist/pick.d.ts +1 -0
- package/dist/pick.js +11 -1
- package/dist/popups.d.ts +17 -0
- package/dist/popups.js +1 -0
- package/dist/project-language.d.ts +8 -0
- package/dist/project-language.js +34 -2
- package/dist/refresher.d.ts +1 -0
- package/dist/refresher.js +5 -0
- package/dist/rename-model.d.ts +1 -1
- package/dist/rename-model.js +3 -7
- package/dist/rename.js +5 -8
- package/dist/rendered-docs.d.ts +62 -0
- package/dist/rendered-docs.js +368 -0
- package/dist/row-host.d.ts +2 -0
- package/dist/row-host.js +11 -3
- package/dist/semantic-delta.d.ts +2 -0
- package/dist/semantic-delta.js +27 -0
- package/dist/semantic-model.js +16 -0
- package/dist/semantic-tokens.d.ts +26 -1
- package/dist/semantic-tokens.js +174 -14
- package/dist/shortcut-keys.d.ts +2 -0
- package/dist/shortcut-keys.js +6 -2
- package/dist/signature.js +1 -1
- package/dist/snippet-field-transforms.d.ts +30 -0
- package/dist/snippet-field-transforms.js +116 -0
- package/dist/snippet-indent.d.ts +2 -0
- package/dist/snippet-indent.js +49 -0
- package/dist/snippet-range-order.d.ts +3 -0
- package/dist/snippet-range-order.js +19 -0
- package/dist/snippet-session.d.ts +42 -6
- package/dist/snippet-session.js +650 -53
- package/dist/snippet-transform-client.d.ts +19 -0
- package/dist/snippet-transform-client.js +66 -0
- package/dist/snippet-transform-format.d.ts +15 -0
- package/dist/snippet-transform-format.js +302 -0
- package/dist/snippet-transform-protocol.d.ts +45 -0
- package/dist/snippet-transform-protocol.js +8 -0
- package/dist/snippet-transform-queue.d.ts +32 -0
- package/dist/snippet-transform-queue.js +173 -0
- package/dist/snippet-transform-worker.d.ts +10 -0
- package/dist/snippet-transform-worker.js +41 -0
- package/dist/snippet-variable-names.d.ts +1 -0
- package/dist/snippet-variable-names.js +36 -0
- package/dist/snippet-variables.d.ts +7 -0
- package/dist/snippet-variables.js +66 -0
- package/dist/snippet.d.ts +30 -3
- package/dist/snippet.js +402 -87
- package/dist/split-alignment.d.ts +6 -0
- package/dist/split-alignment.js +38 -0
- package/dist/split-scroll.d.ts +14 -0
- package/dist/split-scroll.js +45 -0
- package/dist/symbols.d.ts +2 -1
- package/dist/symbols.js +39 -7
- package/dist/testing.d.ts +5 -1
- package/dist/testing.js +12 -0
- package/dist/workspace-edit.d.ts +25 -3
- package/dist/workspace-edit.js +282 -143
- package/package.json +10 -5
- package/src/CallHierarchyPanel.tsx +238 -0
- package/src/CompletionPopup.tsx +12 -1
- package/src/DiffView.tsx +262 -0
- package/src/EditorContextMenu.tsx +1 -0
- package/src/EditorView.tsx +21 -2
- package/src/FindReplace.tsx +51 -13
- package/src/HoverCard.tsx +23 -21
- package/src/LanguagePopups.tsx +5 -0
- package/src/Markdown.tsx +10 -3
- package/src/PeekPanel.tsx +2 -3
- package/src/PickPopup.tsx +18 -2
- package/src/RenameCard.tsx +18 -0
- package/src/RenderedDocs.tsx +155 -0
- package/src/SplitDiffView.tsx +337 -0
- package/src/SymbolPicker.tsx +4 -3
- package/src/call-hierarchy-model.ts +158 -0
- package/src/call-hierarchy.ts +441 -0
- package/src/code-actions-model.ts +17 -11
- package/src/code-actions.ts +3 -1
- package/src/completion-model.ts +61 -35
- package/src/completion.ts +302 -42
- package/src/diagnostics.ts +8 -6
- package/src/diff-expansion.ts +49 -0
- package/src/diff-overlays.tsx +69 -0
- package/src/diff-syntax.ts +67 -0
- package/src/diff-wordmarks.ts +31 -0
- package/src/doc-comment-model.ts +336 -0
- package/src/editor-language.ts +21 -5
- package/src/editor-react.css +79 -0
- package/src/format-on-type.ts +111 -0
- package/src/host-types.ts +19 -0
- package/src/hover.ts +18 -3
- package/src/index.ts +27 -4
- package/src/inlay-hints.ts +394 -18
- package/src/language-ids.ts +4 -2
- package/src/locales/en.json +114 -3
- package/src/locales/nl.json +114 -3
- package/src/models.ts +40 -1
- package/src/patch-decorations.ts +224 -0
- package/src/patch-layout.ts +388 -0
- package/src/peek.ts +1 -0
- package/src/pick.ts +12 -1
- package/src/popups.ts +24 -0
- package/src/project-language.ts +43 -2
- package/src/refresher.ts +6 -0
- package/src/rename-model.ts +4 -8
- package/src/rename.ts +5 -8
- package/src/rendered-docs.ts +422 -0
- package/src/row-host.ts +12 -3
- package/src/semantic-delta.ts +29 -0
- package/src/semantic-model.ts +17 -0
- package/src/semantic-tokens.ts +202 -19
- package/src/shortcut-keys.ts +6 -2
- package/src/signature.ts +1 -1
- package/src/snippet-field-transforms.ts +144 -0
- package/src/snippet-indent.ts +53 -0
- package/src/snippet-range-order.ts +19 -0
- package/src/snippet-session.ts +797 -56
- package/src/snippet-transform-client.ts +84 -0
- package/src/snippet-transform-format.ts +325 -0
- package/src/snippet-transform-protocol.ts +24 -0
- package/src/snippet-transform-queue.ts +199 -0
- package/src/snippet-transform-worker.ts +58 -0
- package/src/snippet-variable-names.ts +36 -0
- package/src/snippet-variables.ts +74 -0
- package/src/snippet.ts +452 -89
- package/src/split-alignment.ts +45 -0
- package/src/split-scroll.ts +54 -0
- package/src/symbols.ts +43 -7
- package/src/testing.ts +25 -0
- package/src/workspace-edit.ts +347 -169
package/src/snippet-session.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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:
|
|
21
|
-
private
|
|
22
|
-
private
|
|
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:
|
|
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
|
-
|
|
28
|
-
|
|
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.
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
if (
|
|
51
|
-
|
|
272
|
+
const frame = frameOf(insertions);
|
|
273
|
+
if (frame === null) {
|
|
274
|
+
if (!options.nested) {
|
|
275
|
+
this.end();
|
|
276
|
+
}
|
|
52
277
|
return;
|
|
53
278
|
}
|
|
54
|
-
|
|
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.
|
|
60
|
-
this.
|
|
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
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
}
|