@adecore/editor-react 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +88 -0
  3. package/dist/CompletionPopup.js +11 -3
  4. package/dist/DiffView.d.ts +22 -0
  5. package/dist/DiffView.js +203 -0
  6. package/dist/EditorContextMenu.js +1 -0
  7. package/dist/EditorView.js +18 -1
  8. package/dist/FindReplace.d.ts +1 -1
  9. package/dist/FindReplace.js +41 -11
  10. package/dist/HoverCard.js +2 -2
  11. package/dist/LanguagePopups.js +4 -1
  12. package/dist/Markdown.d.ts +2 -1
  13. package/dist/Markdown.js +8 -4
  14. package/dist/PeekPanel.d.ts +3 -0
  15. package/dist/PeekPanel.js +2 -3
  16. package/dist/PickPopup.js +15 -2
  17. package/dist/RenameCard.js +10 -5
  18. package/dist/RenderedDocs.d.ts +9 -0
  19. package/dist/RenderedDocs.js +84 -0
  20. package/dist/SplitDiffView.d.ts +9 -0
  21. package/dist/SplitDiffView.js +276 -0
  22. package/dist/SymbolPicker.d.ts +3 -0
  23. package/dist/SymbolPicker.js +4 -3
  24. package/dist/call-hierarchy-model.d.ts +52 -0
  25. package/dist/call-hierarchy-model.js +99 -0
  26. package/dist/call-hierarchy.d.ts +42 -0
  27. package/dist/call-hierarchy.js +399 -0
  28. package/dist/code-actions-model.d.ts +2 -0
  29. package/dist/code-actions-model.js +12 -10
  30. package/dist/code-actions.js +3 -1
  31. package/dist/completion-model.d.ts +14 -4
  32. package/dist/completion-model.js +40 -33
  33. package/dist/completion.d.ts +4 -0
  34. package/dist/completion.js +257 -42
  35. package/dist/diagnostics.js +8 -6
  36. package/dist/diff-expansion.d.ts +9 -0
  37. package/dist/diff-expansion.js +41 -0
  38. package/dist/diff-overlays.d.ts +15 -0
  39. package/dist/diff-overlays.js +28 -0
  40. package/dist/diff-syntax.d.ts +16 -0
  41. package/dist/diff-syntax.js +49 -0
  42. package/dist/diff-wordmarks.d.ts +5 -0
  43. package/dist/diff-wordmarks.js +26 -0
  44. package/dist/doc-comment-model.d.ts +23 -0
  45. package/dist/doc-comment-model.js +291 -0
  46. package/dist/editor-language.d.ts +13 -1
  47. package/dist/editor-language.js +14 -4
  48. package/dist/editor-react.css +79 -0
  49. package/dist/format-on-type.d.ts +14 -0
  50. package/dist/format-on-type.js +96 -0
  51. package/dist/host-types.d.ts +17 -0
  52. package/dist/hover.d.ts +4 -0
  53. package/dist/hover.js +15 -2
  54. package/dist/index.d.ts +19 -5
  55. package/dist/index.js +9 -1
  56. package/dist/inlay-hints.d.ts +47 -2
  57. package/dist/inlay-hints.js +353 -16
  58. package/dist/language-ids.js +4 -2
  59. package/dist/locales/en.json +114 -3
  60. package/dist/locales/nl.json +114 -3
  61. package/dist/models.d.ts +6 -2
  62. package/dist/models.js +5 -1
  63. package/dist/patch-decorations.d.ts +27 -0
  64. package/dist/patch-decorations.js +182 -0
  65. package/dist/patch-layout.d.ts +40 -0
  66. package/dist/patch-layout.js +304 -0
  67. package/dist/peek.js +1 -0
  68. package/dist/pick.d.ts +1 -0
  69. package/dist/pick.js +11 -1
  70. package/dist/popups.d.ts +17 -0
  71. package/dist/popups.js +1 -0
  72. package/dist/project-language.d.ts +8 -0
  73. package/dist/project-language.js +34 -2
  74. package/dist/refresher.d.ts +1 -0
  75. package/dist/refresher.js +5 -0
  76. package/dist/rename-model.d.ts +1 -1
  77. package/dist/rename-model.js +3 -7
  78. package/dist/rename.js +5 -8
  79. package/dist/rendered-docs.d.ts +62 -0
  80. package/dist/rendered-docs.js +368 -0
  81. package/dist/row-host.d.ts +2 -0
  82. package/dist/row-host.js +11 -3
  83. package/dist/semantic-delta.d.ts +2 -0
  84. package/dist/semantic-delta.js +27 -0
  85. package/dist/semantic-model.js +16 -0
  86. package/dist/semantic-tokens.d.ts +26 -1
  87. package/dist/semantic-tokens.js +174 -14
  88. package/dist/shortcut-keys.d.ts +2 -0
  89. package/dist/shortcut-keys.js +6 -2
  90. package/dist/signature.js +1 -1
  91. package/dist/snippet-field-transforms.d.ts +30 -0
  92. package/dist/snippet-field-transforms.js +116 -0
  93. package/dist/snippet-indent.d.ts +2 -0
  94. package/dist/snippet-indent.js +49 -0
  95. package/dist/snippet-range-order.d.ts +3 -0
  96. package/dist/snippet-range-order.js +19 -0
  97. package/dist/snippet-session.d.ts +42 -6
  98. package/dist/snippet-session.js +650 -53
  99. package/dist/snippet-transform-client.d.ts +19 -0
  100. package/dist/snippet-transform-client.js +66 -0
  101. package/dist/snippet-transform-format.d.ts +15 -0
  102. package/dist/snippet-transform-format.js +302 -0
  103. package/dist/snippet-transform-protocol.d.ts +45 -0
  104. package/dist/snippet-transform-protocol.js +8 -0
  105. package/dist/snippet-transform-queue.d.ts +32 -0
  106. package/dist/snippet-transform-queue.js +173 -0
  107. package/dist/snippet-transform-worker.d.ts +10 -0
  108. package/dist/snippet-transform-worker.js +41 -0
  109. package/dist/snippet-variable-names.d.ts +1 -0
  110. package/dist/snippet-variable-names.js +36 -0
  111. package/dist/snippet-variables.d.ts +7 -0
  112. package/dist/snippet-variables.js +66 -0
  113. package/dist/snippet.d.ts +30 -3
  114. package/dist/snippet.js +402 -87
  115. package/dist/split-alignment.d.ts +6 -0
  116. package/dist/split-alignment.js +38 -0
  117. package/dist/split-scroll.d.ts +14 -0
  118. package/dist/split-scroll.js +45 -0
  119. package/dist/symbols.d.ts +2 -1
  120. package/dist/symbols.js +39 -7
  121. package/dist/testing.d.ts +5 -1
  122. package/dist/testing.js +12 -0
  123. package/dist/workspace-edit.d.ts +25 -3
  124. package/dist/workspace-edit.js +282 -143
  125. package/package.json +10 -5
  126. package/src/CallHierarchyPanel.tsx +238 -0
  127. package/src/CompletionPopup.tsx +12 -1
  128. package/src/DiffView.tsx +262 -0
  129. package/src/EditorContextMenu.tsx +1 -0
  130. package/src/EditorView.tsx +21 -2
  131. package/src/FindReplace.tsx +51 -13
  132. package/src/HoverCard.tsx +23 -21
  133. package/src/LanguagePopups.tsx +5 -0
  134. package/src/Markdown.tsx +10 -3
  135. package/src/PeekPanel.tsx +2 -3
  136. package/src/PickPopup.tsx +18 -2
  137. package/src/RenameCard.tsx +18 -0
  138. package/src/RenderedDocs.tsx +155 -0
  139. package/src/SplitDiffView.tsx +337 -0
  140. package/src/SymbolPicker.tsx +4 -3
  141. package/src/call-hierarchy-model.ts +158 -0
  142. package/src/call-hierarchy.ts +441 -0
  143. package/src/code-actions-model.ts +17 -11
  144. package/src/code-actions.ts +3 -1
  145. package/src/completion-model.ts +61 -35
  146. package/src/completion.ts +302 -42
  147. package/src/diagnostics.ts +8 -6
  148. package/src/diff-expansion.ts +49 -0
  149. package/src/diff-overlays.tsx +69 -0
  150. package/src/diff-syntax.ts +67 -0
  151. package/src/diff-wordmarks.ts +31 -0
  152. package/src/doc-comment-model.ts +336 -0
  153. package/src/editor-language.ts +21 -5
  154. package/src/editor-react.css +79 -0
  155. package/src/format-on-type.ts +111 -0
  156. package/src/host-types.ts +19 -0
  157. package/src/hover.ts +18 -3
  158. package/src/index.ts +27 -4
  159. package/src/inlay-hints.ts +394 -18
  160. package/src/language-ids.ts +4 -2
  161. package/src/locales/en.json +114 -3
  162. package/src/locales/nl.json +114 -3
  163. package/src/models.ts +40 -1
  164. package/src/patch-decorations.ts +224 -0
  165. package/src/patch-layout.ts +388 -0
  166. package/src/peek.ts +1 -0
  167. package/src/pick.ts +12 -1
  168. package/src/popups.ts +24 -0
  169. package/src/project-language.ts +43 -2
  170. package/src/refresher.ts +6 -0
  171. package/src/rename-model.ts +4 -8
  172. package/src/rename.ts +5 -8
  173. package/src/rendered-docs.ts +422 -0
  174. package/src/row-host.ts +12 -3
  175. package/src/semantic-delta.ts +29 -0
  176. package/src/semantic-model.ts +17 -0
  177. package/src/semantic-tokens.ts +202 -19
  178. package/src/shortcut-keys.ts +6 -2
  179. package/src/signature.ts +1 -1
  180. package/src/snippet-field-transforms.ts +144 -0
  181. package/src/snippet-indent.ts +53 -0
  182. package/src/snippet-range-order.ts +19 -0
  183. package/src/snippet-session.ts +797 -56
  184. package/src/snippet-transform-client.ts +84 -0
  185. package/src/snippet-transform-format.ts +325 -0
  186. package/src/snippet-transform-protocol.ts +24 -0
  187. package/src/snippet-transform-queue.ts +199 -0
  188. package/src/snippet-transform-worker.ts +58 -0
  189. package/src/snippet-variable-names.ts +36 -0
  190. package/src/snippet-variables.ts +74 -0
  191. package/src/snippet.ts +452 -89
  192. package/src/split-alignment.ts +45 -0
  193. package/src/split-scroll.ts +54 -0
  194. package/src/symbols.ts +43 -7
  195. package/src/testing.ts +25 -0
  196. package/src/workspace-edit.ts +347 -169
package/src/snippet.ts CHANGED
@@ -1,147 +1,510 @@
1
- /* A tab stop of a snippet: its index, and where it stands in the text the snippet inserts. */
2
- export interface SnippetStop {
3
- readonly index: number;
4
- /* UTF-16 offsets into the inserted text; equal for a stop that holds no text. */
1
+ import { withSnippetOrder } from './snippet-range-order.ts';
2
+ import { scanSnippetTransform } from './snippet-transform-format.ts';
3
+ import type { SnippetTransformJob } from './snippet-transform-protocol.ts';
4
+ import { SNIPPET_VARIABLES } from './snippet-variable-names.ts';
5
+
6
+ export type SnippetTransform = Omit<SnippetTransformJob, 'value'>;
7
+
8
+ /* UTF-16 offsets into the inserted text; equal for an empty stop. */
9
+ export interface SnippetRange {
5
10
  readonly start: number;
6
11
  readonly end: number;
12
+ readonly parents?: readonly number[];
13
+ readonly transform?: SnippetTransform;
14
+ /* A descendant of this occurrence begins at its end, so indentation must keep it contained. */
15
+ readonly tail?: true;
16
+ }
17
+
18
+ export interface SnippetStop extends SnippetRange {
19
+ readonly index: number;
20
+ }
21
+
22
+ export interface SnippetField {
23
+ readonly index: number;
24
+ readonly ranges: readonly SnippetRange[];
25
+ /* Index into ranges of the occurrence that defines the default. */
26
+ readonly primary: number;
27
+ /* Child positions projected into copies of a parent, not additional syntactic tab stops. */
28
+ readonly linked?: readonly SnippetRange[];
29
+ readonly parents?: readonly number[];
30
+ readonly choices?: readonly string[];
7
31
  }
8
32
 
9
33
  export interface ParsedSnippet {
34
+ readonly limited?: true;
10
35
  readonly text: string;
11
- /* In text order, a stop of the same index again being a mirror, which is not edited along. */
12
36
  readonly stops: readonly SnippetStop[];
37
+ /* Optional so callers that construct the older text/stops shape remain valid. */
38
+ readonly fields?: readonly SnippetField[];
39
+ readonly transforms?: readonly (SnippetRange & { readonly transform: SnippetTransform })[];
40
+ }
41
+
42
+ type Node =
43
+ | { kind: 'text'; text: string; transform?: SnippetTransform }
44
+ | { kind: 'field'; index: number; body?: Node[]; choices?: string[]; transform?: SnippetTransform }
45
+ | { kind: 'variable'; name: string; body?: Node[]; transform?: SnippetTransform };
46
+
47
+ export type SnippetVariableResolver = (name: string) => string | undefined;
48
+ export interface SnippetTemplate {
49
+ readonly source: string;
50
+ render(resolve?: SnippetVariableResolver, variableTransforms?: readonly string[]): ParsedSnippet;
13
51
  }
52
+ type FieldNode = Extract<Node, { kind: 'field' }>;
14
53
 
15
- const NAME = '[A-Za-z_]\\w*';
16
- const TABSTOP = new RegExp(`^\\$(\\d+)|^\\$\\{(\\d+)\\}`);
17
- const PLACEHOLDER = /^\$\{(\d+):/;
18
- const CHOICE = /^\$\{(\d+)\|/;
19
- const DEFAULT_VARIABLE = new RegExp(`^\\$\\{${NAME}:`);
20
- const TRANSFORMED_VARIABLE = new RegExp(`^\\$\\{${NAME}/`);
21
- const BARE_VARIABLE = new RegExp(`^\\$${NAME}|^\\$\\{${NAME}\\}`);
54
+ export const MAX_SNIPPET_TEXT = 100_000;
55
+ const MAX_SNIPPET_NODES = 10_000;
56
+ const MAX_SNIPPET_WORK = 50_000;
57
+ const MAX_SNIPPET_RANGES = 1_000;
58
+ const MAX_SNIPPET_DEPTH = 64;
59
+
60
+ class SnippetLimitError extends Error {}
61
+ class MalformedSnippetError extends Error {}
22
62
 
23
63
  class SnippetParser {
24
- text = '';
25
- readonly stops: SnippetStop[] = [];
26
64
  private at = 0;
65
+ private nodes = 0;
66
+ private depth = 0;
67
+ private readonly name = /(?:\d+|[A-Za-z_]\w*)/y;
27
68
  private readonly source: string;
28
69
 
29
70
  constructor(source: string) {
30
71
  this.source = source;
31
72
  }
32
73
 
33
- parse(): ParsedSnippet {
34
- this.sequence(false);
35
- return { text: this.text, stops: this.stops };
74
+ parse(): Node[] {
75
+ return this.sequence(false);
36
76
  }
37
77
 
38
- /* Text up to the `}` that closes the placeholder this is in, which is left for the caller. */
39
- private sequence(nested: boolean): void {
40
- while (this.at < this.source.length) {
41
- const character = this.source[this.at]!;
42
- if (character === '}' && nested) {
43
- return;
78
+ private sequence(nested: boolean): Node[] {
79
+ if (++this.depth > MAX_SNIPPET_DEPTH) {
80
+ throw new SnippetLimitError();
81
+ }
82
+ const nodes: Node[] = [];
83
+ let text = '';
84
+ const flush = (): void => {
85
+ if (text !== '') {
86
+ this.push(nodes, { kind: 'text', text });
87
+ text = '';
44
88
  }
89
+ };
90
+ while (this.at < this.source.length && !(nested && this.source[this.at] === '}')) {
91
+ const character = this.source[this.at]!;
45
92
  if (character === '\\' && /[\\$}]/.test(this.source[this.at + 1] ?? '')) {
46
- this.text += this.source[this.at + 1];
93
+ text += this.source[this.at + 1];
47
94
  this.at += 2;
48
95
  } else if (character === '$') {
49
- this.dollar();
96
+ const node = this.dollar();
97
+ if (node === null) {
98
+ text += '$';
99
+ } else {
100
+ flush();
101
+ this.push(nodes, node);
102
+ }
50
103
  } else {
51
- this.text += character;
104
+ text += character;
52
105
  this.at++;
53
106
  }
54
107
  }
108
+ flush();
109
+ this.depth--;
110
+ return nodes;
55
111
  }
56
112
 
57
- private dollar(): void {
58
- const rest = this.source.slice(this.at);
59
- const tabstop = TABSTOP.exec(rest);
60
- const placeholder = PLACEHOLDER.exec(rest);
61
- const choice = CHOICE.exec(rest);
62
- if (tabstop !== null) {
63
- this.at += tabstop[0].length;
64
- this.stops.push({ index: Number(tabstop[1] ?? tabstop[2]), start: this.text.length, end: this.text.length });
65
- } else if (placeholder !== null) {
66
- this.at += placeholder[0].length;
67
- const start = this.text.length;
68
- this.sequence(true);
69
- this.closeBrace();
70
- this.stops.push({ index: Number(placeholder[1]), start, end: this.text.length });
71
- } else if (choice !== null) {
72
- this.at += choice[0].length;
73
- const start = this.text.length;
74
- this.firstChoice();
75
- this.stops.push({ index: Number(choice[1]), start, end: this.text.length });
76
- } else if (DEFAULT_VARIABLE.test(rest)) {
77
- this.at += rest.indexOf(':') + 1;
78
- this.sequence(true);
79
- this.closeBrace();
80
- } else if (TRANSFORMED_VARIABLE.test(rest)) {
81
- this.skipBraces();
82
- } else if (BARE_VARIABLE.test(rest)) {
83
- this.at += BARE_VARIABLE.exec(rest)![0].length;
84
- } else {
85
- this.text += '$';
86
- this.at++;
113
+ private push(nodes: Node[], node: Node): void {
114
+ if (++this.nodes > MAX_SNIPPET_NODES) {
115
+ throw new SnippetLimitError();
87
116
  }
117
+ nodes.push(node);
88
118
  }
89
119
 
90
- private closeBrace(): void {
91
- if (this.source[this.at] === '}') {
120
+ private dollar(): Node | null {
121
+ const start = this.at++;
122
+ const brace = this.source[this.at] === '{';
123
+ if (brace) {
124
+ this.at++;
125
+ }
126
+ this.name.lastIndex = this.at;
127
+ const name = this.name.exec(this.source)?.[0];
128
+ if (name === undefined) {
129
+ this.at = start + 1;
130
+ return null;
131
+ }
132
+ const index = /^\d+$/.test(name) ? Number(name) : null;
133
+ if (index !== null && !Number.isSafeInteger(index)) {
134
+ this.at = start + 1;
135
+ return null;
136
+ }
137
+ this.at += name.length;
138
+ const field = (): FieldNode => ({ kind: 'field', index: index! });
139
+ if (!brace) {
140
+ return index === null ? { kind: 'variable', name } : field();
141
+ }
142
+ const next = this.source[this.at];
143
+ if (next === '}') {
92
144
  this.at++;
145
+ return index === null ? { kind: 'variable', name } : field();
93
146
  }
147
+ if (next === ':') {
148
+ this.at++;
149
+ const body = this.sequence(true);
150
+ if (this.source[this.at] === '}') {
151
+ this.at++;
152
+ }
153
+ return index === null ? { kind: 'variable', name, body } : { ...field(), body };
154
+ }
155
+ if (next === '|' && index !== null) {
156
+ this.at++;
157
+ return { ...field(), choices: this.choices() };
158
+ }
159
+ if (next === '/') {
160
+ try {
161
+ const { end, ...transform } = scanSnippetTransform(this.source, this.at);
162
+ this.at = end;
163
+ return index === null ? { kind: 'variable', name, transform } : { ...field(), transform };
164
+ } catch (error) {
165
+ if (error instanceof RangeError) {
166
+ throw new SnippetLimitError();
167
+ }
168
+ if (error instanceof SyntaxError) {
169
+ throw new MalformedSnippetError();
170
+ }
171
+ throw error;
172
+ }
173
+ }
174
+ this.at = start + 1;
175
+ return null;
94
176
  }
95
177
 
96
- /* The first of `a,b,c|}`, and the rest dropped. */
97
- private firstChoice(): void {
98
- let first = true;
178
+ private choices(): string[] {
179
+ const values = [''];
99
180
  while (this.at < this.source.length && !(this.source[this.at] === '|' && this.source[this.at + 1] === '}')) {
100
- const character = this.source[this.at]!;
101
- if (character === '\\' && /[\\,|]/.test(this.source[this.at + 1] ?? '')) {
102
- this.at++;
103
- if (first) {
104
- this.text += this.source[this.at];
105
- }
181
+ const character = this.source[this.at++]!;
182
+ if (character === '\\' && /[\\,|]/.test(this.source[this.at] ?? '')) {
183
+ values[values.length - 1] += this.source[this.at++]!;
106
184
  } else if (character === ',') {
107
- first = false;
108
- } else if (first) {
109
- this.text += character;
185
+ if (values.length >= MAX_SNIPPET_NODES) {
186
+ throw new SnippetLimitError();
187
+ }
188
+ values.push('');
189
+ } else {
190
+ values[values.length - 1] += character;
110
191
  }
111
- this.at++;
112
192
  }
113
- this.at += 2;
193
+ this.at = Math.min(this.source.length, this.at + 2);
194
+ return values;
114
195
  }
196
+ }
115
197
 
116
- /* Past the `}` that closes a `${name/regex/format/}` variable, which inserts nothing here. */
117
- private skipBraces(): void {
118
- let depth = 0;
119
- while (this.at < this.source.length) {
120
- const character = this.source[this.at]!;
121
- this.at += character === '\\' ? 2 : 1;
122
- if (character === '{') {
123
- depth++;
124
- } else if (character === '}' && --depth === 0) {
125
- return;
198
+ function renderSnippet(nodes: readonly Node[], variableTransforms?: readonly string[]): ParsedSnippet {
199
+ const definitions = new Map<number, FieldNode>();
200
+ const collect = (nodes: readonly Node[]): void => {
201
+ for (const node of nodes) {
202
+ if (node.kind === 'field') {
203
+ const known = definitions.get(node.index);
204
+ if (
205
+ known === undefined ||
206
+ (known.body === undefined &&
207
+ known.choices === undefined &&
208
+ (node.body !== undefined || node.choices !== undefined || (known.transform !== undefined && node.transform === undefined)))
209
+ ) {
210
+ definitions.set(node.index, node);
211
+ }
212
+ }
213
+ if (node.kind !== 'text' && node.body !== undefined) {
214
+ collect(node.body);
126
215
  }
127
216
  }
217
+ };
218
+ collect(nodes);
219
+ const fields = new Map<
220
+ number,
221
+ { index: number; ranges: SnippetRange[]; linked: SnippetRange[]; primary: number; parents: Set<number>; choices?: string[] }
222
+ >();
223
+ const stops: SnippetStop[] = [];
224
+ const transforms: (SnippetRange & { transform: SnippetTransform })[] = [];
225
+ let text = '';
226
+ let work = 0;
227
+ let ranges = 0;
228
+ let occurrence = 0;
229
+ let tailEvent = 0;
230
+ let tailOffset = -1;
231
+ const recordTail = (range: SnippetRange): void => {
232
+ if (range.start === range.end || range.tail === true) {
233
+ tailEvent++;
234
+ tailOffset = range.end;
235
+ }
236
+ };
237
+ const append = (value: string): void => {
238
+ if (text.length + value.length > MAX_SNIPPET_TEXT) {
239
+ throw new SnippetLimitError();
240
+ }
241
+ text += value;
242
+ };
243
+ const render = (nodes: readonly Node[], linked: boolean, parents: readonly number[], visiting: ReadonlySet<number>): void => {
244
+ if (parents.length > MAX_SNIPPET_DEPTH) {
245
+ throw new SnippetLimitError();
246
+ }
247
+ for (const node of nodes) {
248
+ if (++work > MAX_SNIPPET_WORK) {
249
+ throw new SnippetLimitError();
250
+ }
251
+ const order = occurrence++;
252
+ if (node.kind === 'text') {
253
+ const start = text.length;
254
+ const value = node.transform === undefined || variableTransforms === undefined ? node.text : variableTransforms[transforms.length];
255
+ if (typeof value !== 'string') {
256
+ throw new RangeError('The variable transform count does not match the snippet.');
257
+ }
258
+ append(value);
259
+ if (node.transform !== undefined) {
260
+ if (++ranges > MAX_SNIPPET_RANGES) {
261
+ throw new SnippetLimitError();
262
+ }
263
+ const range = { start, end: text.length, transform: node.transform, ...(parents.length === 0 ? {} : { parents: [...parents] }) };
264
+ withSnippetOrder(range, order);
265
+ transforms.push(range);
266
+ recordTail(range);
267
+ }
268
+ continue;
269
+ }
270
+ if (node.kind === 'variable') {
271
+ render(node.body ?? [], linked, parents, visiting);
272
+ continue;
273
+ }
274
+ if (++ranges > MAX_SNIPPET_RANGES) {
275
+ throw new SnippetLimitError();
276
+ }
277
+ const definition = definitions.get(node.index)!;
278
+ const from = text.length;
279
+ const beforeTail = tailEvent;
280
+ if (!visiting.has(node.index)) {
281
+ if (definition.choices !== undefined) {
282
+ append(definition.choices[0] ?? '');
283
+ } else {
284
+ render(definition.body ?? [], linked || node !== definition, [...parents, node.index], new Set([...visiting, node.index]));
285
+ }
286
+ }
287
+ const range: SnippetRange = {
288
+ start: from,
289
+ end: text.length,
290
+ ...(parents.length === 0 ? {} : { parents: [...parents] }),
291
+ ...(node.transform === undefined ? {} : { transform: node.transform }),
292
+ ...(tailEvent !== beforeTail && tailOffset === text.length ? { tail: true as const } : {})
293
+ };
294
+ withSnippetOrder(range, order);
295
+ recordTail(range);
296
+ const field = fields.get(node.index) ?? {
297
+ index: node.index,
298
+ ranges: [],
299
+ linked: [],
300
+ primary: 0,
301
+ parents: new Set<number>(),
302
+ choices: definition.choices
303
+ };
304
+ parents.filter((parent) => parent !== node.index).forEach((parent) => field.parents.add(parent));
305
+ if (linked) {
306
+ field.linked.push(range);
307
+ } else {
308
+ if (node === definition) {
309
+ field.primary = field.ranges.length;
310
+ }
311
+ field.ranges.push(range);
312
+ stops.push(
313
+ withSnippetOrder(
314
+ {
315
+ index: node.index,
316
+ start: range.start,
317
+ end: range.end,
318
+ ...(node.transform === undefined ? {} : { transform: node.transform }),
319
+ ...(range.tail === undefined ? {} : { tail: range.tail })
320
+ },
321
+ order
322
+ )
323
+ );
324
+ }
325
+ fields.set(node.index, field);
326
+ }
327
+ };
328
+ render(nodes, false, [], new Set());
329
+ if (variableTransforms !== undefined && variableTransforms.length !== transforms.length) {
330
+ throw new RangeError('The variable transform count does not match the snippet.');
331
+ }
332
+ stops.sort((left, right) => left.start - right.start || right.end - left.end);
333
+ return {
334
+ text,
335
+ stops,
336
+ ...(transforms.length === 0 ? {} : { transforms }),
337
+ fields: [...fields.values()]
338
+ .filter((field) => field.ranges.length > 0)
339
+ .map((field) => ({
340
+ index: field.index,
341
+ ranges: field.ranges,
342
+ primary: field.primary,
343
+ ...(field.linked.length === 0 ? {} : { linked: field.linked }),
344
+ ...(field.parents.size === 0 ? {} : { parents: [...field.parents] }),
345
+ ...(field.choices === undefined ? {} : { choices: field.choices })
346
+ }))
347
+ };
348
+ }
349
+
350
+ function variableIndices(nodes: readonly Node[]): ReadonlyMap<string, number> {
351
+ let highest = 0;
352
+ const names = new Set<string>();
353
+ const scan = (nodes: readonly Node[]): void => {
354
+ for (const node of nodes) {
355
+ if (node.kind === 'field') {
356
+ highest = Math.max(highest, node.index);
357
+ } else if (node.kind === 'variable' && !SNIPPET_VARIABLES.has(node.name)) {
358
+ names.add(node.name);
359
+ }
360
+ if (node.kind !== 'text' && node.body !== undefined) {
361
+ scan(node.body);
362
+ }
363
+ }
364
+ };
365
+ scan(nodes);
366
+ return new Map(
367
+ [...names].map((name) => {
368
+ if (!Number.isSafeInteger(++highest)) {
369
+ throw new SnippetLimitError();
370
+ }
371
+ return [name, highest];
372
+ })
373
+ );
374
+ }
375
+
376
+ function resolveVariables(nodes: readonly Node[], indices: ReadonlyMap<string, number>, resolve?: SnippetVariableResolver): Node[] {
377
+ const unknown = new Map<string, { index: number; body: Node[]; defaulted: boolean; definition: Node }>();
378
+ const values = new Map<string, string | undefined>();
379
+ const valueOf = (name: string): string | undefined => {
380
+ if (!values.has(name)) {
381
+ values.set(name, resolve?.(name));
382
+ }
383
+ return values.get(name);
384
+ };
385
+ const collect = (nodes: readonly Node[]): void => {
386
+ for (const node of nodes) {
387
+ if (node.kind === 'variable' && valueOf(node.name) === undefined && !SNIPPET_VARIABLES.has(node.name)) {
388
+ const previous = unknown.get(node.name);
389
+ if (previous === undefined) {
390
+ unknown.set(node.name, {
391
+ index: indices.get(node.name)!,
392
+ body: node.body ?? [{ kind: 'text', text: node.name }],
393
+ defaulted: node.body !== undefined,
394
+ definition: node
395
+ });
396
+ } else if (node.body !== undefined && !previous.defaulted) {
397
+ previous.body = node.body;
398
+ previous.defaulted = true;
399
+ previous.definition = node;
400
+ } else if (!previous.defaulted && node.transform === undefined && previous.definition.transform !== undefined) {
401
+ previous.definition = node;
402
+ }
403
+ }
404
+ if (node.kind !== 'text' && node.body !== undefined) {
405
+ collect(node.body);
406
+ }
407
+ }
408
+ };
409
+ collect(nodes);
410
+ // Resolved empty variables retain their defaults without adding a navigable field.
411
+ let work = 0;
412
+ const normalize = (nodes: readonly Node[], visiting: ReadonlySet<string>, depth = 0): Node[] => {
413
+ if (depth > MAX_SNIPPET_DEPTH) {
414
+ throw new SnippetLimitError();
415
+ }
416
+ return nodes.flatMap((node): Node[] => {
417
+ if (++work > MAX_SNIPPET_WORK) {
418
+ throw new SnippetLimitError();
419
+ }
420
+ if (node.kind === 'variable' && !unknown.has(node.name)) {
421
+ const value = valueOf(node.name);
422
+ if (node.transform !== undefined) {
423
+ return [{ kind: 'text', text: value ?? '', transform: node.transform }];
424
+ }
425
+ return value !== undefined && value !== '' ? [{ kind: 'text', text: value }] : normalize(node.body ?? [], visiting, depth + 1);
426
+ }
427
+ if (node.kind === 'field') {
428
+ return [{ ...node, ...(node.body === undefined ? {} : { body: normalize(node.body, visiting, depth + 1) }) }];
429
+ }
430
+ if (node.kind === 'variable') {
431
+ const field = unknown.get(node.name)!;
432
+ return [
433
+ {
434
+ kind: 'field',
435
+ index: field.index,
436
+ ...(node.transform === undefined ? {} : { transform: node.transform }),
437
+ ...(node !== field.definition
438
+ ? {}
439
+ : { body: visiting.has(node.name) ? [] : normalize(field.body, new Set([...visiting, node.name]), depth + 1) })
440
+ }
441
+ ];
442
+ }
443
+ return [node];
444
+ });
445
+ };
446
+ return normalize(nodes, new Set());
447
+ }
448
+
449
+ export function compileSnippet(source: string): SnippetTemplate {
450
+ let nodes: Node[] | null = null;
451
+ let indices: ReadonlyMap<string, number> = new Map();
452
+ if (source.length <= MAX_SNIPPET_TEXT) {
453
+ try {
454
+ nodes = new SnippetParser(source).parse();
455
+ indices = variableIndices(nodes);
456
+ } catch (error) {
457
+ if (error instanceof MalformedSnippetError) {
458
+ return { source, render: () => ({ text: source, stops: [] }) };
459
+ }
460
+ if (!(error instanceof SnippetLimitError)) {
461
+ throw error;
462
+ }
463
+ nodes = null;
464
+ }
128
465
  }
466
+ return {
467
+ source,
468
+ render: (resolve, variableTransforms) => {
469
+ if (nodes === null) {
470
+ return { text: source, stops: [], limited: true };
471
+ }
472
+ try {
473
+ return renderSnippet(resolveVariables(nodes, indices, resolve), variableTransforms);
474
+ } catch (error) {
475
+ if (!(error instanceof SnippetLimitError)) {
476
+ throw error;
477
+ }
478
+ // Language-server snippets may expand exponentially; retain the original text without a session.
479
+ return { text: source, stops: [], limited: true };
480
+ }
481
+ }
482
+ };
129
483
  }
130
484
 
131
- export function parseSnippet(snippet: string): ParsedSnippet {
132
- return new SnippetParser(snippet).parse();
485
+ export function parseSnippet(snippet: string, resolve?: SnippetVariableResolver): ParsedSnippet {
486
+ return compileSnippet(snippet).render(resolve);
133
487
  }
134
488
 
135
- /* The stops in the order Tab visits them: 1, 2 and on, one per index, then the final stop `$0`, which the end of the text stands in for. */
489
+ /* One defining occurrence per index, then $0 or the end of the inserted text. */
136
490
  export function tabOrder(parsed: ParsedSnippet): SnippetStop[] {
137
491
  const first = new Map<number, SnippetStop>();
138
492
  for (const stop of parsed.stops) {
139
- const known = first.get(stop.index);
140
- if (known === undefined || stop.start < known.start) {
493
+ if (!first.has(stop.index)) {
141
494
  first.set(stop.index, stop);
142
495
  }
143
496
  }
497
+ for (const field of parsed.fields ?? []) {
498
+ const range = field.ranges[field.primary];
499
+ if (range !== undefined) {
500
+ first.set(field.index, {
501
+ index: field.index,
502
+ start: range.start,
503
+ end: range.end,
504
+ ...(range.transform === undefined ? {} : { transform: range.transform })
505
+ });
506
+ }
507
+ }
144
508
  const visited = [...first.values()].filter((stop) => stop.index > 0).sort((left, right) => left.index - right.index);
145
- const final = first.get(0) ?? { index: 0, start: parsed.text.length, end: parsed.text.length };
146
- return [...visited, final];
509
+ return [...visited, first.get(0) ?? { index: 0, start: parsed.text.length, end: parsed.text.length }];
147
510
  }
@@ -0,0 +1,45 @@
1
+ import type { DiffSplitRow } from '@adecore/merge';
2
+ import type { HostedRow } from './row-host.ts';
3
+
4
+ /* Intrinsic heights exclude spacers, so repeated alignment converges without adding its own padding. */
5
+ export function splitAlignment(
6
+ aligned: readonly DiffSplitRow[],
7
+ oldHeight: (line: number) => number,
8
+ newHeight: (line: number) => number,
9
+ gapHeight = 32
10
+ ): { old: HostedRow[]; new: HostedRow[] } {
11
+ const rows: { old: HostedRow[]; new: HostedRow[] } = { old: [], new: [] };
12
+ const previous = { old: -1, new: -1 };
13
+ const add = (side: 'old' | 'new', index: number, height: number, gap?: number): void => {
14
+ if (height <= 0) {
15
+ return;
16
+ }
17
+ const line = Math.max(0, previous[side]);
18
+ const placement = previous[side] < 0 ? 'above' : 'below';
19
+ const last = rows[side].at(-1);
20
+ if (gap === undefined && last?.filler && last.line === line && last.placement === placement) {
21
+ last.height += height;
22
+ } else {
23
+ rows[side].push({ id: gap === undefined ? `spacer-${index}` : `gap-${gap}`, line, placement, height, fixed: true, filler: gap === undefined });
24
+ }
25
+ };
26
+ for (const [index, entry] of aligned.entries()) {
27
+ if (entry.kind === 'gap') {
28
+ add('old', index, gapHeight, entry.row.id);
29
+ add('new', index, gapHeight, entry.row.id);
30
+ continue;
31
+ }
32
+ const old = entry.old === undefined ? 0 : oldHeight(entry.old);
33
+ const fresh = entry.new === undefined ? 0 : newHeight(entry.new);
34
+ if (entry.old !== undefined) {
35
+ previous.old = entry.old;
36
+ }
37
+ if (entry.new !== undefined) {
38
+ previous.new = entry.new;
39
+ }
40
+ const target = Math.max(old, fresh);
41
+ add('old', index, target - old);
42
+ add('new', index, target - fresh);
43
+ }
44
+ return rows;
45
+ }