@adecore/editor-react 0.22.1 → 0.23.0

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