@adecore/editor-react 0.39.0 → 0.40.1

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 (154) hide show
  1. package/dist/CallHierarchyPanel.js +30 -55
  2. package/dist/CodeAuthorsCard.js +0 -1
  3. package/dist/CompletionPopup.js +150 -181
  4. package/dist/DiffView.d.ts +1 -1
  5. package/dist/DiffView.js +2 -29
  6. package/dist/PickPopup.js +54 -80
  7. package/dist/SplitDiffView.js +27 -57
  8. package/dist/SymbolPicker.js +2 -8
  9. package/dist/code-actions-model.js +2 -9
  10. package/dist/code-actions.js +10 -11
  11. package/dist/code-vision.js +2 -1
  12. package/dist/completion-model.js +2 -2
  13. package/dist/completion.js +13 -34
  14. package/dist/definition-link.d.ts +1 -0
  15. package/dist/definition-link.js +22 -18
  16. package/dist/diagnostics-model.d.ts +1 -0
  17. package/dist/diagnostics-model.js +3 -1
  18. package/dist/diagnostics.js +1 -2
  19. package/dist/diff-syntax.d.ts +1 -0
  20. package/dist/diff-syntax.js +31 -0
  21. package/dist/doc-comment-model.d.ts +1 -0
  22. package/dist/doc-comment-model.js +1 -1
  23. package/dist/folding.js +2 -3
  24. package/dist/format-on-type.js +6 -10
  25. package/dist/hover-content.d.ts +0 -2
  26. package/dist/hover-content.js +1 -9
  27. package/dist/inlay-hints.d.ts +0 -1
  28. package/dist/inlay-hints.js +11 -41
  29. package/dist/navigation.js +4 -14
  30. package/dist/peek.js +2 -12
  31. package/dist/popup-focus.d.ts +3 -1
  32. package/dist/popup-focus.js +33 -0
  33. package/dist/recent-choices.d.ts +0 -1
  34. package/dist/recent-choices.js +0 -4
  35. package/dist/rendered-docs.d.ts +1 -0
  36. package/dist/rendered-docs.js +15 -28
  37. package/dist/snippet-field-transforms.d.ts +5 -0
  38. package/dist/snippet-field-transforms.js +27 -16
  39. package/dist/snippet-session.js +8 -20
  40. package/dist/symbol-links.js +3 -10
  41. package/dist/symbol-picker-model.d.ts +1 -0
  42. package/dist/symbol-picker-model.js +7 -0
  43. package/dist/unique.d.ts +10 -0
  44. package/dist/unique.js +15 -0
  45. package/package.json +9 -9
  46. package/src/AnchoredPopup.tsx +2 -0
  47. package/src/BaselineStatus.tsx +1 -0
  48. package/src/CallHierarchyPanel.tsx +16 -6
  49. package/src/ChangeReview.tsx +7 -0
  50. package/src/CodeAuthorsCard.tsx +2 -5
  51. package/src/CompletionPopup.tsx +21 -15
  52. package/src/DiffView.tsx +53 -30
  53. package/src/EditorContextMenu.tsx +7 -0
  54. package/src/EditorView.tsx +28 -0
  55. package/src/FindReplace.tsx +8 -0
  56. package/src/HoverCard.tsx +8 -0
  57. package/src/HoverSections.tsx +11 -0
  58. package/src/LanguagePopups.tsx +1 -0
  59. package/src/PanelResizeHandle.tsx +8 -0
  60. package/src/PathText.tsx +1 -0
  61. package/src/PeekPanel.tsx +13 -0
  62. package/src/PickPopup.tsx +7 -6
  63. package/src/RenameCard.tsx +13 -0
  64. package/src/RenderedDocs.tsx +11 -0
  65. package/src/SplitDiffView.tsx +91 -56
  66. package/src/SymbolPicker.tsx +10 -18
  67. package/src/call-hierarchy-model.ts +10 -0
  68. package/src/call-hierarchy.ts +82 -0
  69. package/src/code-actions-model.ts +21 -9
  70. package/src/code-actions.ts +83 -7
  71. package/src/code-authors.ts +20 -0
  72. package/src/code-vision-model.ts +37 -0
  73. package/src/code-vision.ts +47 -3
  74. package/src/completion-call.ts +19 -0
  75. package/src/completion-model.ts +58 -2
  76. package/src/completion.ts +156 -30
  77. package/src/conflict-model.ts +4 -0
  78. package/src/context-menu-rows.ts +1 -0
  79. package/src/context-menu.ts +7 -0
  80. package/src/definition-link.ts +42 -18
  81. package/src/diagnostics-model.ts +19 -1
  82. package/src/diagnostics.ts +31 -2
  83. package/src/diff-expansion.ts +9 -0
  84. package/src/diff-overlays.tsx +8 -0
  85. package/src/diff-syntax.ts +54 -0
  86. package/src/diff-wordmarks.ts +8 -0
  87. package/src/doc-comment-model.ts +51 -1
  88. package/src/editor-language.ts +11 -0
  89. package/src/find-query.ts +7 -0
  90. package/src/folding.ts +10 -3
  91. package/src/format-on-type.ts +23 -10
  92. package/src/highlight-layers.ts +3 -0
  93. package/src/highlights.ts +18 -0
  94. package/src/history.ts +11 -0
  95. package/src/holder.ts +3 -0
  96. package/src/hover-content.ts +34 -17
  97. package/src/hover.ts +57 -0
  98. package/src/index.ts +4 -0
  99. package/src/inlay-hints.ts +86 -47
  100. package/src/key-priority.ts +2 -0
  101. package/src/line-edits.ts +8 -0
  102. package/src/models.ts +6 -0
  103. package/src/navigation-history.ts +5 -0
  104. package/src/navigation.ts +28 -13
  105. package/src/panel-size.ts +7 -0
  106. package/src/patch-decorations.ts +39 -0
  107. package/src/patch-layout.ts +47 -0
  108. package/src/peek-model.ts +21 -0
  109. package/src/peek.ts +49 -13
  110. package/src/pick.ts +35 -0
  111. package/src/popup-focus.ts +11 -1
  112. package/src/popup-placement.ts +5 -0
  113. package/src/popups.ts +2 -6
  114. package/src/project-language.ts +33 -0
  115. package/src/project-problems.ts +11 -0
  116. package/src/proposal-model.ts +40 -0
  117. package/src/recent-choices.ts +3 -5
  118. package/src/refresher.ts +3 -0
  119. package/src/rename-model.ts +28 -0
  120. package/src/rename.ts +56 -0
  121. package/src/rendered-docs.ts +89 -19
  122. package/src/review-group.ts +4 -0
  123. package/src/row-host.ts +12 -0
  124. package/src/selection-ranges.ts +6 -0
  125. package/src/semantic-delta.ts +7 -0
  126. package/src/semantic-model.ts +11 -0
  127. package/src/semantic-tokens.ts +37 -0
  128. package/src/shortcut-keys.ts +3 -0
  129. package/src/signature-model.ts +7 -0
  130. package/src/signature.ts +27 -0
  131. package/src/snippet-field-transforms.ts +48 -13
  132. package/src/snippet-indent.ts +10 -0
  133. package/src/snippet-range-order.ts +5 -0
  134. package/src/snippet-session.ts +173 -31
  135. package/src/snippet-transform-client.ts +14 -0
  136. package/src/snippet-transform-format.ts +98 -0
  137. package/src/snippet-transform-protocol.ts +5 -0
  138. package/src/snippet-transform-queue.ts +53 -0
  139. package/src/snippet-transform-worker.ts +14 -0
  140. package/src/snippet-variables.ts +9 -0
  141. package/src/snippet.ts +107 -0
  142. package/src/split-alignment.ts +10 -0
  143. package/src/split-scroll.ts +8 -0
  144. package/src/symbol-links.ts +9 -12
  145. package/src/symbol-picker-model.ts +25 -0
  146. package/src/symbol-picker.ts +9 -0
  147. package/src/symbols.ts +21 -0
  148. package/src/testing.ts +7 -0
  149. package/src/timers.ts +5 -0
  150. package/src/unique.ts +23 -0
  151. package/src/use-problem-counts.ts +4 -0
  152. package/src/view-state.ts +9 -0
  153. package/src/word-diff.ts +5 -0
  154. package/src/workspace-edit.ts +84 -0
package/src/snippet.ts CHANGED
@@ -45,13 +45,16 @@ type Node =
45
45
  | { kind: 'variable'; name: string; body?: Node[]; transform?: SnippetTransform };
46
46
 
47
47
  export type SnippetVariableResolver = (name: string) => string | undefined;
48
+
48
49
  export interface SnippetTemplate {
49
50
  readonly source: string;
50
51
  render(resolve?: SnippetVariableResolver, variableTransforms?: readonly string[]): ParsedSnippet;
51
52
  }
53
+
52
54
  type FieldNode = Extract<Node, { kind: 'field' }>;
53
55
 
54
56
  export const MAX_SNIPPET_TEXT = 100_000;
57
+
55
58
  const MAX_SNIPPET_NODES = 10_000;
56
59
  const MAX_SNIPPET_WORK = 50_000;
57
60
  const MAX_SNIPPET_RANGES = 1_000;
@@ -79,21 +82,26 @@ class SnippetParser {
79
82
  if (++this.depth > MAX_SNIPPET_DEPTH) {
80
83
  throw new SnippetLimitError();
81
84
  }
85
+
82
86
  const nodes: Node[] = [];
83
87
  let text = '';
88
+
84
89
  const flush = (): void => {
85
90
  if (text !== '') {
86
91
  this.push(nodes, { kind: 'text', text });
87
92
  text = '';
88
93
  }
89
94
  };
95
+
90
96
  while (this.at < this.source.length && !(nested && this.source[this.at] === '}')) {
91
97
  const character = this.source[this.at]!;
98
+
92
99
  if (character === '\\' && /[\\$}]/.test(this.source[this.at + 1] ?? '')) {
93
100
  text += this.source[this.at + 1];
94
101
  this.at += 2;
95
102
  } else if (character === '$') {
96
103
  const node = this.dollar();
104
+
97
105
  if (node === null) {
98
106
  text += '$';
99
107
  } else {
@@ -105,8 +113,10 @@ class SnippetParser {
105
113
  this.at++;
106
114
  }
107
115
  }
116
+
108
117
  flush();
109
118
  this.depth--;
119
+
110
120
  return nodes;
111
121
  }
112
122
 
@@ -114,93 +124,125 @@ class SnippetParser {
114
124
  if (++this.nodes > MAX_SNIPPET_NODES) {
115
125
  throw new SnippetLimitError();
116
126
  }
127
+
117
128
  nodes.push(node);
118
129
  }
119
130
 
120
131
  private dollar(): Node | null {
121
132
  const start = this.at++;
122
133
  const brace = this.source[this.at] === '{';
134
+
123
135
  if (brace) {
124
136
  this.at++;
125
137
  }
138
+
126
139
  this.name.lastIndex = this.at;
127
140
  const name = this.name.exec(this.source)?.[0];
141
+
128
142
  if (name === undefined) {
129
143
  this.at = start + 1;
144
+
130
145
  return null;
131
146
  }
147
+
132
148
  const index = /^\d+$/.test(name) ? Number(name) : null;
149
+
133
150
  if (index !== null && !Number.isSafeInteger(index)) {
134
151
  this.at = start + 1;
152
+
135
153
  return null;
136
154
  }
155
+
137
156
  this.at += name.length;
157
+
138
158
  const field = (): FieldNode => ({ kind: 'field', index: index! });
159
+
139
160
  if (!brace) {
140
161
  return index === null ? { kind: 'variable', name } : field();
141
162
  }
163
+
142
164
  const next = this.source[this.at];
165
+
143
166
  if (next === '}') {
144
167
  this.at++;
168
+
145
169
  return index === null ? { kind: 'variable', name } : field();
146
170
  }
171
+
147
172
  if (next === ':') {
148
173
  this.at++;
149
174
  const body = this.sequence(true);
175
+
150
176
  if (this.source[this.at] === '}') {
151
177
  this.at++;
152
178
  }
179
+
153
180
  return index === null ? { kind: 'variable', name, body } : { ...field(), body };
154
181
  }
182
+
155
183
  if (next === '|' && index !== null) {
156
184
  this.at++;
185
+
157
186
  return { ...field(), choices: this.choices() };
158
187
  }
188
+
159
189
  if (next === '/') {
160
190
  try {
161
191
  const { end, ...transform } = scanSnippetTransform(this.source, this.at);
162
192
  this.at = end;
193
+
163
194
  return index === null ? { kind: 'variable', name, transform } : { ...field(), transform };
164
195
  } catch (error) {
165
196
  if (error instanceof RangeError) {
166
197
  throw new SnippetLimitError();
167
198
  }
199
+
168
200
  if (error instanceof SyntaxError) {
169
201
  throw new MalformedSnippetError();
170
202
  }
203
+
171
204
  throw error;
172
205
  }
173
206
  }
207
+
174
208
  this.at = start + 1;
209
+
175
210
  return null;
176
211
  }
177
212
 
178
213
  private choices(): string[] {
179
214
  const values = [''];
215
+
180
216
  while (this.at < this.source.length && !(this.source[this.at] === '|' && this.source[this.at + 1] === '}')) {
181
217
  const character = this.source[this.at++]!;
218
+
182
219
  if (character === '\\' && /[\\,|]/.test(this.source[this.at] ?? '')) {
183
220
  values[values.length - 1] += this.source[this.at++]!;
184
221
  } else if (character === ',') {
185
222
  if (values.length >= MAX_SNIPPET_NODES) {
186
223
  throw new SnippetLimitError();
187
224
  }
225
+
188
226
  values.push('');
189
227
  } else {
190
228
  values[values.length - 1] += character;
191
229
  }
192
230
  }
231
+
193
232
  this.at = Math.min(this.source.length, this.at + 2);
233
+
194
234
  return values;
195
235
  }
196
236
  }
197
237
 
198
238
  function renderSnippet(nodes: readonly Node[], variableTransforms?: readonly string[]): ParsedSnippet {
199
239
  const definitions = new Map<number, FieldNode>();
240
+
200
241
  const collect = (nodes: readonly Node[]): void => {
201
242
  for (const node of nodes) {
202
243
  if (node.kind === 'field') {
203
244
  const known = definitions.get(node.index);
245
+
204
246
  if (
205
247
  known === undefined ||
206
248
  (known.body === undefined &&
@@ -210,16 +252,20 @@ function renderSnippet(nodes: readonly Node[], variableTransforms?: readonly str
210
252
  definitions.set(node.index, node);
211
253
  }
212
254
  }
255
+
213
256
  if (node.kind !== 'text' && node.body !== undefined) {
214
257
  collect(node.body);
215
258
  }
216
259
  }
217
260
  };
261
+
218
262
  collect(nodes);
263
+
219
264
  const fields = new Map<
220
265
  number,
221
266
  { index: number; ranges: SnippetRange[]; linked: SnippetRange[]; primary: number; parents: Set<number>; choices?: string[] }
222
267
  >();
268
+
223
269
  const stops: SnippetStop[] = [];
224
270
  const transforms: (SnippetRange & { transform: SnippetTransform })[] = [];
225
271
  let text = '';
@@ -228,55 +274,72 @@ function renderSnippet(nodes: readonly Node[], variableTransforms?: readonly str
228
274
  let occurrence = 0;
229
275
  let tailEvent = 0;
230
276
  let tailOffset = -1;
277
+
231
278
  const recordTail = (range: SnippetRange): void => {
232
279
  if (range.start === range.end || range.tail === true) {
233
280
  tailEvent++;
234
281
  tailOffset = range.end;
235
282
  }
236
283
  };
284
+
237
285
  const append = (value: string): void => {
238
286
  if (text.length + value.length > MAX_SNIPPET_TEXT) {
239
287
  throw new SnippetLimitError();
240
288
  }
289
+
241
290
  text += value;
242
291
  };
292
+
243
293
  const render = (nodes: readonly Node[], linked: boolean, parents: readonly number[], visiting: ReadonlySet<number>): void => {
244
294
  if (parents.length > MAX_SNIPPET_DEPTH) {
245
295
  throw new SnippetLimitError();
246
296
  }
297
+
247
298
  for (const node of nodes) {
248
299
  if (++work > MAX_SNIPPET_WORK) {
249
300
  throw new SnippetLimitError();
250
301
  }
302
+
251
303
  const order = occurrence++;
304
+
252
305
  if (node.kind === 'text') {
253
306
  const start = text.length;
254
307
  const value = node.transform === undefined || variableTransforms === undefined ? node.text : variableTransforms[transforms.length];
308
+
255
309
  if (typeof value !== 'string') {
256
310
  throw new RangeError('The variable transform count does not match the snippet.');
257
311
  }
312
+
258
313
  append(value);
314
+
259
315
  if (node.transform !== undefined) {
260
316
  if (++ranges > MAX_SNIPPET_RANGES) {
261
317
  throw new SnippetLimitError();
262
318
  }
319
+
263
320
  const range = { start, end: text.length, transform: node.transform, ...(parents.length === 0 ? {} : { parents: [...parents] }) };
264
321
  withSnippetOrder(range, order);
265
322
  transforms.push(range);
266
323
  recordTail(range);
267
324
  }
325
+
268
326
  continue;
269
327
  }
328
+
270
329
  if (node.kind === 'variable') {
271
330
  render(node.body ?? [], linked, parents, visiting);
331
+
272
332
  continue;
273
333
  }
334
+
274
335
  if (++ranges > MAX_SNIPPET_RANGES) {
275
336
  throw new SnippetLimitError();
276
337
  }
338
+
277
339
  const definition = definitions.get(node.index)!;
278
340
  const from = text.length;
279
341
  const beforeTail = tailEvent;
342
+
280
343
  if (!visiting.has(node.index)) {
281
344
  if (definition.choices !== undefined) {
282
345
  append(definition.choices[0] ?? '');
@@ -284,6 +347,7 @@ function renderSnippet(nodes: readonly Node[], variableTransforms?: readonly str
284
347
  render(definition.body ?? [], linked || node !== definition, [...parents, node.index], new Set([...visiting, node.index]));
285
348
  }
286
349
  }
350
+
287
351
  const range: SnippetRange = {
288
352
  start: from,
289
353
  end: text.length,
@@ -291,8 +355,10 @@ function renderSnippet(nodes: readonly Node[], variableTransforms?: readonly str
291
355
  ...(node.transform === undefined ? {} : { transform: node.transform }),
292
356
  ...(tailEvent !== beforeTail && tailOffset === text.length ? { tail: true as const } : {})
293
357
  };
358
+
294
359
  withSnippetOrder(range, order);
295
360
  recordTail(range);
361
+
296
362
  const field = fields.get(node.index) ?? {
297
363
  index: node.index,
298
364
  ranges: [],
@@ -301,14 +367,18 @@ function renderSnippet(nodes: readonly Node[], variableTransforms?: readonly str
301
367
  parents: new Set<number>(),
302
368
  choices: definition.choices
303
369
  };
370
+
304
371
  parents.filter((parent) => parent !== node.index).forEach((parent) => field.parents.add(parent));
372
+
305
373
  if (linked) {
306
374
  field.linked.push(range);
307
375
  } else {
308
376
  if (node === definition) {
309
377
  field.primary = field.ranges.length;
310
378
  }
379
+
311
380
  field.ranges.push(range);
381
+
312
382
  stops.push(
313
383
  withSnippetOrder(
314
384
  {
@@ -322,14 +392,19 @@ function renderSnippet(nodes: readonly Node[], variableTransforms?: readonly str
322
392
  )
323
393
  );
324
394
  }
395
+
325
396
  fields.set(node.index, field);
326
397
  }
327
398
  };
399
+
328
400
  render(nodes, false, [], new Set());
401
+
329
402
  if (variableTransforms !== undefined && variableTransforms.length !== transforms.length) {
330
403
  throw new RangeError('The variable transform count does not match the snippet.');
331
404
  }
405
+
332
406
  stops.sort((left, right) => left.start - right.start || right.end - left.end);
407
+
333
408
  return {
334
409
  text,
335
410
  stops,
@@ -350,6 +425,7 @@ function renderSnippet(nodes: readonly Node[], variableTransforms?: readonly str
350
425
  function variableIndices(nodes: readonly Node[]): ReadonlyMap<string, number> {
351
426
  let highest = 0;
352
427
  const names = new Set<string>();
428
+
353
429
  const scan = (nodes: readonly Node[]): void => {
354
430
  for (const node of nodes) {
355
431
  if (node.kind === 'field') {
@@ -357,17 +433,21 @@ function variableIndices(nodes: readonly Node[]): ReadonlyMap<string, number> {
357
433
  } else if (node.kind === 'variable' && !SNIPPET_VARIABLES.has(node.name)) {
358
434
  names.add(node.name);
359
435
  }
436
+
360
437
  if (node.kind !== 'text' && node.body !== undefined) {
361
438
  scan(node.body);
362
439
  }
363
440
  }
364
441
  };
442
+
365
443
  scan(nodes);
444
+
366
445
  return new Map(
367
446
  [...names].map((name) => {
368
447
  if (!Number.isSafeInteger(++highest)) {
369
448
  throw new SnippetLimitError();
370
449
  }
450
+
371
451
  return [name, highest];
372
452
  })
373
453
  );
@@ -376,16 +456,20 @@ function variableIndices(nodes: readonly Node[]): ReadonlyMap<string, number> {
376
456
  function resolveVariables(nodes: readonly Node[], indices: ReadonlyMap<string, number>, resolve?: SnippetVariableResolver): Node[] {
377
457
  const unknown = new Map<string, { index: number; body: Node[]; defaulted: boolean; definition: Node }>();
378
458
  const values = new Map<string, string | undefined>();
459
+
379
460
  const valueOf = (name: string): string | undefined => {
380
461
  if (!values.has(name)) {
381
462
  values.set(name, resolve?.(name));
382
463
  }
464
+
383
465
  return values.get(name);
384
466
  };
467
+
385
468
  const collect = (nodes: readonly Node[]): void => {
386
469
  for (const node of nodes) {
387
470
  if (node.kind === 'variable' && valueOf(node.name) === undefined && !SNIPPET_VARIABLES.has(node.name)) {
388
471
  const previous = unknown.get(node.name);
472
+
389
473
  if (previous === undefined) {
390
474
  unknown.set(node.name, {
391
475
  index: indices.get(node.name)!,
@@ -401,34 +485,44 @@ function resolveVariables(nodes: readonly Node[], indices: ReadonlyMap<string, n
401
485
  previous.definition = node;
402
486
  }
403
487
  }
488
+
404
489
  if (node.kind !== 'text' && node.body !== undefined) {
405
490
  collect(node.body);
406
491
  }
407
492
  }
408
493
  };
494
+
409
495
  collect(nodes);
410
496
  // Resolved empty variables retain their defaults without adding a navigable field.
411
497
  let work = 0;
498
+
412
499
  const normalize = (nodes: readonly Node[], visiting: ReadonlySet<string>, depth = 0): Node[] => {
413
500
  if (depth > MAX_SNIPPET_DEPTH) {
414
501
  throw new SnippetLimitError();
415
502
  }
503
+
416
504
  return nodes.flatMap((node): Node[] => {
417
505
  if (++work > MAX_SNIPPET_WORK) {
418
506
  throw new SnippetLimitError();
419
507
  }
508
+
420
509
  if (node.kind === 'variable' && !unknown.has(node.name)) {
421
510
  const value = valueOf(node.name);
511
+
422
512
  if (node.transform !== undefined) {
423
513
  return [{ kind: 'text', text: value ?? '', transform: node.transform }];
424
514
  }
515
+
425
516
  return value !== undefined && value !== '' ? [{ kind: 'text', text: value }] : normalize(node.body ?? [], visiting, depth + 1);
426
517
  }
518
+
427
519
  if (node.kind === 'field') {
428
520
  return [{ ...node, ...(node.body === undefined ? {} : { body: normalize(node.body, visiting, depth + 1) }) }];
429
521
  }
522
+
430
523
  if (node.kind === 'variable') {
431
524
  const field = unknown.get(node.name)!;
525
+
432
526
  return [
433
527
  {
434
528
  kind: 'field',
@@ -440,15 +534,18 @@ function resolveVariables(nodes: readonly Node[], indices: ReadonlyMap<string, n
440
534
  }
441
535
  ];
442
536
  }
537
+
443
538
  return [node];
444
539
  });
445
540
  };
541
+
446
542
  return normalize(nodes, new Set());
447
543
  }
448
544
 
449
545
  export function compileSnippet(source: string): SnippetTemplate {
450
546
  let nodes: Node[] | null = null;
451
547
  let indices: ReadonlyMap<string, number> = new Map();
548
+
452
549
  if (source.length <= MAX_SNIPPET_TEXT) {
453
550
  try {
454
551
  nodes = new SnippetParser(source).parse();
@@ -457,24 +554,29 @@ export function compileSnippet(source: string): SnippetTemplate {
457
554
  if (error instanceof MalformedSnippetError) {
458
555
  return { source, render: () => ({ text: source, stops: [] }) };
459
556
  }
557
+
460
558
  if (!(error instanceof SnippetLimitError)) {
461
559
  throw error;
462
560
  }
561
+
463
562
  nodes = null;
464
563
  }
465
564
  }
565
+
466
566
  return {
467
567
  source,
468
568
  render: (resolve, variableTransforms) => {
469
569
  if (nodes === null) {
470
570
  return { text: source, stops: [], limited: true };
471
571
  }
572
+
472
573
  try {
473
574
  return renderSnippet(resolveVariables(nodes, indices, resolve), variableTransforms);
474
575
  } catch (error) {
475
576
  if (!(error instanceof SnippetLimitError)) {
476
577
  throw error;
477
578
  }
579
+
478
580
  // Language-server snippets may expand exponentially; retain the original text without a session.
479
581
  return { text: source, stops: [], limited: true };
480
582
  }
@@ -489,13 +591,16 @@ export function parseSnippet(snippet: string, resolve?: SnippetVariableResolver)
489
591
  /* One defining occurrence per index, then $0 or the end of the inserted text. */
490
592
  export function tabOrder(parsed: ParsedSnippet): SnippetStop[] {
491
593
  const first = new Map<number, SnippetStop>();
594
+
492
595
  for (const stop of parsed.stops) {
493
596
  if (!first.has(stop.index)) {
494
597
  first.set(stop.index, stop);
495
598
  }
496
599
  }
600
+
497
601
  for (const field of parsed.fields ?? []) {
498
602
  const range = field.ranges[field.primary];
603
+
499
604
  if (range !== undefined) {
500
605
  first.set(field.index, {
501
606
  index: field.index,
@@ -505,6 +610,8 @@ export function tabOrder(parsed: ParsedSnippet): SnippetStop[] {
505
610
  });
506
611
  }
507
612
  }
613
+
508
614
  const visited = [...first.values()].filter((stop) => stop.index > 0).sort((left, right) => left.index - right.index);
615
+
509
616
  return [...visited, first.get(0) ?? { index: 0, start: parsed.text.length, end: parsed.text.length }];
510
617
  }
@@ -10,36 +10,46 @@ export function splitAlignment(
10
10
  ): { old: HostedRow[]; new: HostedRow[] } {
11
11
  const rows: { old: HostedRow[]; new: HostedRow[] } = { old: [], new: [] };
12
12
  const previous = { old: -1, new: -1 };
13
+
13
14
  const add = (side: 'old' | 'new', index: number, height: number, gap?: number): void => {
14
15
  if (height <= 0) {
15
16
  return;
16
17
  }
18
+
17
19
  const line = Math.max(0, previous[side]);
18
20
  const placement = previous[side] < 0 ? 'above' : 'below';
19
21
  const last = rows[side].at(-1);
22
+
20
23
  if (gap === undefined && last?.filler && last.line === line && last.placement === placement) {
21
24
  last.height += height;
22
25
  } else {
23
26
  rows[side].push({ id: gap === undefined ? `spacer-${index}` : `gap-${gap}`, line, placement, height, fixed: true, filler: gap === undefined });
24
27
  }
25
28
  };
29
+
26
30
  for (const [index, entry] of aligned.entries()) {
27
31
  if (entry.kind === 'gap') {
28
32
  add('old', index, gapHeight, entry.row.id);
29
33
  add('new', index, gapHeight, entry.row.id);
34
+
30
35
  continue;
31
36
  }
37
+
32
38
  const old = entry.old === undefined ? 0 : oldHeight(entry.old);
33
39
  const fresh = entry.new === undefined ? 0 : newHeight(entry.new);
40
+
34
41
  if (entry.old !== undefined) {
35
42
  previous.old = entry.old;
36
43
  }
44
+
37
45
  if (entry.new !== undefined) {
38
46
  previous.new = entry.new;
39
47
  }
48
+
40
49
  const target = Math.max(old, fresh);
41
50
  add('old', index, target - old);
42
51
  add('new', index, target - fresh);
43
52
  }
53
+
44
54
  return rows;
45
55
  }
@@ -23,14 +23,18 @@ export class SplitScroll {
23
23
  if (this.syncing) {
24
24
  return;
25
25
  }
26
+
26
27
  const top = this.editors[side].getScrollTop();
28
+
27
29
  if (side !== this.leader) {
28
30
  // Native scroll echoes arrive after setScrollTop returns, including a clamped follower position.
29
31
  if (Math.abs(top - this.expected[side]) < 1) {
30
32
  return;
31
33
  }
34
+
32
35
  this.leader = side;
33
36
  }
37
+
34
38
  this.sync();
35
39
  }
36
40
 
@@ -39,11 +43,15 @@ export class SplitScroll {
39
43
  const other = side === 'old' ? 'new' : 'old';
40
44
  const top = this.editors[side].getScrollTop();
41
45
  this.expected[side] = top;
46
+
42
47
  if (Math.abs(this.editors[other].getScrollTop() - top) < 1) {
43
48
  this.expected[other] = this.editors[other].getScrollTop();
49
+
44
50
  return;
45
51
  }
52
+
46
53
  this.syncing = true;
54
+
47
55
  try {
48
56
  this.editors[other].setScrollTop(top);
49
57
  } finally {
@@ -1,4 +1,5 @@
1
1
  import type { Location, SymbolInformation, WorkspaceSymbol, WorkspaceSymbolResult } from '@adecore/lsp';
2
+ import { uniqueBy } from './unique.ts';
2
3
 
3
4
  /* The classes a name in a signature has: a pointer and an underline on hover, and the accent only then, so the signature stays calm. */
4
5
  export const SYMBOL_LINK_CLASS = 'cursor-pointer hover:text-accent hover:underline';
@@ -46,27 +47,23 @@ export function placesOfName(result: WorkspaceSymbolResult, name: string, fileTe
46
47
  const found: Found[] = ((result ?? []) as (SymbolInformation | WorkspaceSymbol)[]).filter((symbol) => symbol.name === name);
47
48
  const types = found.filter((symbol) => TYPE_KINDS.has(symbol.kind));
48
49
  let candidates = types.length > 0 ? types : found;
50
+
49
51
  if (candidates.length > 1) {
50
52
  const named = candidates.filter(
51
53
  (symbol) => symbol.containerName !== undefined && symbol.containerName !== '' && fileText.includes(symbol.containerName)
52
54
  );
55
+
53
56
  if (named.length > 0) {
54
57
  candidates = named;
55
58
  }
56
59
  }
57
- const seen = new Set<string>();
58
- return candidates
59
- .map((symbol): Location =>
60
+
61
+ return uniqueBy(
62
+ candidates.map((symbol): Location =>
60
63
  'range' in symbol.location
61
64
  ? symbol.location
62
65
  : { uri: symbol.location.uri, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } }
63
- )
64
- .filter((location) => {
65
- const key = `${location.uri}\0${location.range.start.line}`;
66
- if (seen.has(key)) {
67
- return false;
68
- }
69
- seen.add(key);
70
- return true;
71
- });
66
+ ),
67
+ (location) => `${location.uri}\0${location.range.start.line}`
68
+ );
72
69
  }