@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
@@ -35,36 +35,47 @@ const ALIASES: Readonly<Record<string, string>> = {
35
35
 
36
36
  /* Tags that carry a type before their subject. */
37
37
  const TYPED = new Set(['param', 'property', 'return', 'throws', 'var', 'type', 'yield', 'template', 'this']);
38
+
38
39
  /* Tags whose first word is the thing they are about. */
39
40
  const SUBJECT_FIRST = new Set(['param', 'property', 'var', 'template']);
40
41
 
41
42
  const TAG_START = /^@([A-Za-z][\w-]*)(?::[\w-]+)?[ \t]*(.*)$/;
42
43
  const FENCE = /^\s*(```|~~~)/;
43
44
  const DASH = /^[—–-][ \t]+/;
45
+
44
46
  const DANGEROUS =
45
47
  /<(script|style|iframe|object|embed|svg|math|form)\b[\s\S]*?<\/\1\s*>|<\/?(script|style|iframe|object|embed|link|meta|base|svg|math|form|img|video|audio|source|input|button|textarea|select|canvas|frame|frameset)\b[^>]*>/gi;
48
+
46
49
  const SAFE_SCHEME = /^(https?:|mailto:)/i;
47
50
 
48
51
  /* Reads a type that starts the text: in braces as JSDoc writes it, or as one word with its brackets as PHPDoc does. */
49
52
  function takeType(text: string, braces: boolean): { type: string | null; rest: string } {
50
53
  const trimmed = text.trimStart();
54
+
51
55
  if (trimmed.startsWith('{')) {
52
56
  let depth = 0;
57
+
53
58
  for (let i = 0; i < trimmed.length; i++) {
54
59
  depth += trimmed[i] === '{' ? 1 : trimmed[i] === '}' ? -1 : 0;
60
+
55
61
  if (depth === 0) {
56
62
  return { type: trimmed.slice(1, i).trim(), rest: trimmed.slice(i + 1).trimStart() };
57
63
  }
58
64
  }
65
+
59
66
  return { type: null, rest: trimmed };
60
67
  }
68
+
61
69
  if (braces || /^[$&.]/.test(trimmed)) {
62
70
  return { type: null, rest: trimmed };
63
71
  }
72
+
64
73
  let depth = 0;
65
74
  let end = 0;
75
+
66
76
  for (; end < trimmed.length; end++) {
67
77
  const character = trimmed[end]!;
78
+
68
79
  if ('<([{'.includes(character)) {
69
80
  depth++;
70
81
  } else if ('>)]}'.includes(character)) {
@@ -73,19 +84,24 @@ function takeType(text: string, braces: boolean): { type: string | null; rest: s
73
84
  break;
74
85
  }
75
86
  }
87
+
76
88
  return { type: end === 0 ? null : trimmed.slice(0, end), rest: trimmed.slice(end).trimStart() };
77
89
  }
78
90
 
79
91
  /* The first word of a tag's text: a name with its sigils and the `[name=default]` form of JSDoc. */
80
92
  function takeSubject(text: string): { subject: string | null; rest: string } {
81
93
  const optional = /^\[([^\]]+)\]\s*/.exec(text);
94
+
82
95
  if (optional !== null) {
83
96
  return { subject: optional[1]!, rest: text.slice(optional[0].length) };
84
97
  }
98
+
85
99
  const word = /^(?:\.\.\.|&)?[$\w.\\:-]+/.exec(text);
100
+
86
101
  if (word === null) {
87
102
  return { subject: null, rest: text };
88
103
  }
104
+
89
105
  return { subject: word[0], rest: text.slice(word[0].length).trimStart() };
90
106
  }
91
107
 
@@ -94,11 +110,14 @@ function splitTags(body: string): { description: string; raw: { name: string; te
94
110
  const description: string[] = [];
95
111
  const raw: { name: string; text: string }[] = [];
96
112
  let fenced = false;
113
+
97
114
  for (const line of body.split('\n')) {
98
115
  if (FENCE.test(line)) {
99
116
  fenced = !fenced;
100
117
  }
118
+
101
119
  const start = fenced ? null : TAG_START.exec(line.trimStart());
120
+
102
121
  if (start !== null) {
103
122
  raw.push({ name: start[1]!.toLowerCase(), text: start[2]! });
104
123
  } else if (raw.length > 0) {
@@ -107,6 +126,7 @@ function splitTags(body: string): { description: string; raw: { name: string; te
107
126
  description.push(line);
108
127
  }
109
128
  }
129
+
110
130
  return { description: description.join('\n').trim(), raw };
111
131
  }
112
132
 
@@ -114,10 +134,12 @@ function entryOf(name: string, text: string, php: boolean): DocTagEntry {
114
134
  let rest = text.trim();
115
135
  let type: string | null = null;
116
136
  let subject: string | null = null;
137
+
117
138
  if (TYPED.has(name)) {
118
139
  const taken = name === 'return' && rest.startsWith('$this') ? { type: '$this', rest: rest.slice(5).trimStart() } : takeType(rest, !php);
119
140
  type = taken.type;
120
141
  rest = taken.rest;
142
+
121
143
  // A type that stands alone is the whole text of a tag that has no subject.
122
144
  if (SUBJECT_FIRST.has(name) || (name === 'throws' && type === null)) {
123
145
  const word = takeSubject(rest);
@@ -129,10 +151,12 @@ function entryOf(name: string, text: string, php: boolean): DocTagEntry {
129
151
  subject = target === null ? null : target[0];
130
152
  rest = target === null ? rest : rest.slice(target[0].length).trimStart();
131
153
  }
154
+
132
155
  if (name === 'throws' && type === null && subject !== null) {
133
156
  type = subject;
134
157
  subject = null;
135
158
  }
159
+
136
160
  return { name, type, subject, text: rest.replace(DASH, '').trim() };
137
161
  }
138
162
 
@@ -151,16 +175,19 @@ export function decodeEntities(text: string): string {
151
175
  return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, code: string) => {
152
176
  if (code[0] === '#') {
153
177
  const point = code[1]!.toLowerCase() === 'x' ? Number.parseInt(code.slice(2), 16) : Number.parseInt(code.slice(1), 10);
178
+
154
179
  return Number.isFinite(point) && point > 0 && point <= 0x10ffff ? String.fromCodePoint(point) : match;
155
180
  }
181
+
156
182
  return ENTITIES[code.toLowerCase()] ?? match;
157
183
  });
158
184
  }
159
185
 
160
- function dedent(text: string): string {
186
+ export function dedent(text: string): string {
161
187
  const lines = text.replace(/^\n+|\s+$/g, '').split('\n');
162
188
  const indents = lines.filter((line) => line.trim() !== '').map((line) => line.length - line.trimStart().length);
163
189
  const common = indents.length === 0 ? 0 : Math.min(...indents);
190
+
164
191
  return lines.map((line) => line.slice(common)).join('\n');
165
192
  }
166
193
 
@@ -175,6 +202,7 @@ function stripTags(html: string): string {
175
202
  /* The address of a link when it is one a reader may follow; null for everything else, such as a script address. */
176
203
  export function safeAddress(address: string): string | null {
177
204
  const trimmed = decodeEntities(address.trim());
205
+
178
206
  return SAFE_SCHEME.test(trimmed) || trimmed.startsWith(DOC_LINK_PREFIX) ? trimmed.replace(/\s/g, '%20').replace(/\(/g, '%28').replace(/\)/g, '%29') : null;
179
207
  }
180
208
 
@@ -188,12 +216,15 @@ export function htmlToMarkdown(source: string): string {
188
216
  if (!source.includes('<') && !source.includes('&')) {
189
217
  return source;
190
218
  }
219
+
191
220
  const lists: ('ul' | 'ol')[] = [];
192
221
  const counters: number[] = [];
193
222
  let anchor: string | null = null;
223
+
194
224
  const blocks = source
195
225
  .replace(/<pre\b[^>]*>([\s\S]*?)<\/pre\s*>/gi, (_match, body: string) => fenced(body))
196
226
  .replace(/<code>([^<]*\n[^<]*)<\/code>/gi, (_match, body: string) => fenced(body));
227
+
197
228
  const converted = outsideCode(blocks, (text) =>
198
229
  text
199
230
  .replace(DANGEROUS, '')
@@ -201,6 +232,7 @@ export function htmlToMarkdown(source: string): string {
201
232
  /<(\/?)(p|br|b|strong|i|em|u|code|tt|kbd|samp|var|a|ul|ol|li|h[1-6]|blockquote|hr|div|span|dl|dt|dd|sup|sub|small|center)\b([^>]*?)(\/?)>/gi,
202
233
  (_match, close: string, tag: string, attributes: string) => {
203
234
  const closing = close === '/';
235
+
204
236
  switch (tag.toLowerCase()) {
205
237
  case 'p':
206
238
  case 'div':
@@ -226,10 +258,13 @@ export function htmlToMarkdown(source: string): string {
226
258
  if (closing) {
227
259
  const target = anchor;
228
260
  anchor = null;
261
+
229
262
  return target === null ? '' : `](${target})`;
230
263
  }
264
+
231
265
  const href = /\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(attributes);
232
266
  anchor = href === null ? null : safeAddress(href[1] ?? href[2] ?? href[3] ?? '');
267
+
233
268
  return anchor === null ? '' : '[';
234
269
  }
235
270
  case 'ul':
@@ -241,13 +276,16 @@ export function htmlToMarkdown(source: string): string {
241
276
  lists.push(tag.toLowerCase() as 'ul' | 'ol');
242
277
  counters.push(0);
243
278
  }
279
+
244
280
  return '\n';
245
281
  case 'li': {
246
282
  if (closing) {
247
283
  return '';
248
284
  }
285
+
249
286
  const depth = Math.max(0, lists.length - 1);
250
287
  const marker = lists.at(-1) === 'ol' ? `${(counters[depth] = (counters[depth] ?? 0) + 1)}.` : '-';
288
+
251
289
  return `\n${' '.repeat(depth)}${marker} `;
252
290
  }
253
291
  case 'blockquote':
@@ -260,11 +298,13 @@ export function htmlToMarkdown(source: string): string {
260
298
  if (/^h[1-6]$/i.test(tag)) {
261
299
  return closing ? '\n\n' : `\n\n${'#'.repeat(Number(tag[1]))} `;
262
300
  }
301
+
263
302
  return '';
264
303
  }
265
304
  }
266
305
  )
267
306
  );
307
+
268
308
  // Entities last, so `&lt;b&gt;` stays the text it spells and is not read as a tag above.
269
309
  return outsideCode(converted, decodeEntities).replace(/\n{3,}/g, '\n\n');
270
310
  }
@@ -279,6 +319,7 @@ export function inlineTagsToMarkdown(markdown: string): string {
279
319
  .replace(/\{@(?:link|linkcode|linkplain|see)\s+([^}\s|]+)(?:\s*[|\s]\s*([^}]+?))?\s*\}/g, (_match, target: string, label: string | undefined) => {
280
320
  const address = /^https?:\/\//i.test(target) ? safeAddress(target) : `${DOC_LINK_PREFIX}${encodeURIComponent(target)}`;
281
321
  const shown = label ?? target;
322
+
282
323
  return address === null ? `\`${shown}\`` : `[${label === undefined ? `\`${shown}\`` : shown}](${address})`;
283
324
  })
284
325
  .replace(/\{@code\s+([^}]*)\}/g, (_match, code: string) => `\`${code.trim()}\``)
@@ -290,6 +331,7 @@ export function inlineTagsToMarkdown(markdown: string): string {
290
331
  function exampleOf(text: string): string {
291
332
  const code = text.replace(/^\s*<caption>[\s\S]*?<\/caption>[ \t]*\n?/i, '');
292
333
  const [head = '', ...tail] = code.split('\n');
334
+
293
335
  return head.trim() === '' ? dedent(code) : [head.trim(), dedent(tail.join('\n'))].join('\n').trim();
294
336
  }
295
337
 
@@ -301,16 +343,20 @@ function proseOf(text: string): string {
301
343
  export function parseDocComment(body: string, options: { php?: boolean } = {}): ParsedDocComment {
302
344
  const php = options.php === true;
303
345
  const { description, raw } = splitTags(body);
346
+
304
347
  const tags = raw.map(({ name, text }) => {
305
348
  const bare = name.replace(/^(?:psalm|phpstan)-/, '');
306
349
  const canonical = ALIASES[bare] ?? bare;
307
350
  const entry = entryOf(canonical, text, php);
351
+
308
352
  // An example is code: it keeps its lines and gets no prose treatment, only its caption removed.
309
353
  if (canonical === 'example') {
310
354
  return { ...entry, text: exampleOf(text) };
311
355
  }
356
+
312
357
  return { ...entry, text: proseOf(entry.text) };
313
358
  });
359
+
314
360
  return { description: proseOf(description), tags };
315
361
  }
316
362
 
@@ -325,12 +371,16 @@ export interface DocTagSection {
325
371
  /* The tags gathered by name, so a list of parameters is one section, in reading order. */
326
372
  export function sectionsOf(tags: readonly DocTagEntry[]): DocTagSection[] {
327
373
  const byName = new Map<string, DocTagEntry[]>();
374
+
328
375
  for (const tag of tags) {
329
376
  byName.set(tag.name, [...(byName.get(tag.name) ?? []), tag]);
330
377
  }
378
+
331
379
  const rank = (name: string): number => {
332
380
  const index = ORDER.indexOf(name);
381
+
333
382
  return index === -1 ? ORDER.length : index;
334
383
  };
384
+
335
385
  return [...byName].map(([name, entries]) => ({ name, tags: entries })).sort((left, right) => rank(left.name) - rank(right.name));
336
386
  }
@@ -50,8 +50,10 @@ export class EditorLanguage {
50
50
  readonly project: ProjectLanguage;
51
51
  readonly document: LanguageDocumentHandle;
52
52
  readonly languageId: string;
53
+
53
54
  /* What the features of this editor have open over it: the hover card, the suggestions, the signature. */
54
55
  readonly popups = createPopupStore();
56
+
55
57
  readonly diagnostics: DiagnosticsFeature;
56
58
  readonly hover: HoverFeature;
57
59
  readonly inlayHints: InlayHintsFeature;
@@ -161,6 +163,7 @@ export class EditorLanguage {
161
163
  */
162
164
  onProvidersChanged(listener: () => void): () => void {
163
165
  let listening = true;
166
+
164
167
  void this.document.ready.then(
165
168
  () => {
166
169
  if (listening && !this.disposed) {
@@ -169,11 +172,13 @@ export class EditorLanguage {
169
172
  },
170
173
  () => undefined
171
174
  );
175
+
172
176
  const providers = this.project.service.onProvidersChanged((uri) => {
173
177
  if (uri === this.uri) {
174
178
  listener();
175
179
  }
176
180
  });
181
+
177
182
  return () => {
178
183
  listening = false;
179
184
  providers.dispose();
@@ -197,20 +202,26 @@ export class EditorLanguage {
197
202
  if (this.disposed) {
198
203
  return;
199
204
  }
205
+
200
206
  this.disposed = true;
207
+
201
208
  for (const dispose of this.disposers.splice(0).reverse()) {
202
209
  dispose();
203
210
  }
211
+
204
212
  this.document.release();
205
213
  }
206
214
 
207
215
  /* A file opened from another one lands on the line; this puts the caret on the column the jump meant. */
208
216
  private placeOpenedCaret(position: EditorPosition): void {
209
217
  const column = this.project.takeCaret(this.uri, position.line);
218
+
210
219
  if (column !== null && column.character !== position.character) {
211
220
  this.editor.setCaret(column);
221
+
212
222
  return;
213
223
  }
224
+
214
225
  this.project.noteCaret({ uri: this.uri, position });
215
226
  }
216
227
  }
package/src/find-query.ts CHANGED
@@ -34,10 +34,12 @@ export function compileFind(query: FindQuery): CompiledFind {
34
34
  if (query.text === '') {
35
35
  return { kind: 'empty' };
36
36
  }
37
+
37
38
  const body = query.regex ? query.text : escapeRegExp(query.text);
38
39
  const source = query.wholeWord ? `(?<!${WORD_CHAR})(?:${body})(?!${WORD_CHAR})` : body;
39
40
  // Multiline, so `^` and `$` in a pattern mean a line, the way an editor's find reads them.
40
41
  const flags = `gmu${query.caseSensitive ? '' : 'i'}`;
42
+
41
43
  try {
42
44
  return { kind: 'pattern', pattern: new RegExp(source, flags) };
43
45
  } catch {
@@ -50,6 +52,7 @@ export function matchesIn(text: string, pattern: RegExp, limit: number = MATCH_L
50
52
  const matches: TextMatch[] = [];
51
53
  const global = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
52
54
  let found = global.exec(text);
55
+
53
56
  while (found !== null && matches.length < limit) {
54
57
  if (found[0] === '') {
55
58
  // A `u` pattern moves a lastIndex inside a surrogate pair back to its start, so one step would never leave it.
@@ -57,8 +60,10 @@ export function matchesIn(text: string, pattern: RegExp, limit: number = MATCH_L
57
60
  } else {
58
61
  matches.push({ start: found.index, end: found.index + found[0].length });
59
62
  }
63
+
60
64
  found = global.exec(text);
61
65
  }
66
+
62
67
  return matches;
63
68
  }
64
69
 
@@ -67,8 +72,10 @@ export function stepIndex(current: number | null, count: number, direction: 1 |
67
72
  if (count === 0) {
68
73
  return null;
69
74
  }
75
+
70
76
  if (current === null) {
71
77
  return direction === 1 ? 0 : count - 1;
72
78
  }
79
+
73
80
  return (current + direction + count) % count;
74
81
  }
package/src/folding.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { EditorFoldHints, EditorFoldRangeHint, EditorFoldSymbol } from '@adecore/editor';
1
+ import type { EditorFoldRangeHint, EditorFoldSymbol } from '@adecore/editor';
2
2
  import type { DocumentSymbolResult, FoldingRange } from '@adecore/lsp';
3
3
  import type { EditorLanguage } from './editor-language.ts';
4
4
  import { Refresher } from './refresher.ts';
@@ -27,6 +27,7 @@ const BODIES: Readonly<Record<number, EditorFoldSymbol['body']>> = {
27
27
  export function foldSymbolsOf(result: DocumentSymbolResult): EditorFoldSymbol[] {
28
28
  return symbolSpansOf(result).flatMap(({ kind, range }) => {
29
29
  const body = BODIES[kind];
30
+
30
31
  return body !== undefined && range.end.line > range.start.line ? [{ range, body }] : [];
31
32
  });
32
33
  }
@@ -58,12 +59,15 @@ export class FoldingFeature {
58
59
  constructor(language: EditorLanguage, onSymbols: (listener: (result: DocumentSymbolResult) => void) => () => void, timers: Timers = realTimers) {
59
60
  this.language = language;
60
61
  const { editor, project, uri } = language;
62
+
61
63
  this.refresher = new Refresher(
62
64
  async (signal) => {
63
65
  if (!project.service.supports(METHOD, uri)) {
64
66
  return;
65
67
  }
68
+
66
69
  const ranges = await project.service.foldingRanges(uri, { signal });
70
+
67
71
  if (!signal.aborted) {
68
72
  this.ranges = foldRangesOf(ranges);
69
73
  this.publish();
@@ -72,14 +76,18 @@ export class FoldingFeature {
72
76
  PAUSE_MS,
73
77
  timers
74
78
  );
79
+
75
80
  const symbols = onSymbols((result) => {
76
81
  this.symbols = foldSymbolsOf(result);
77
82
  this.publish();
78
83
  });
84
+
79
85
  const edits = editor.onTextChange(() => this.refresher.later());
86
+
80
87
  const providers = language.onProvidersChanged(() => {
81
88
  this.refresher.now();
82
89
  });
90
+
83
91
  language.onDispose(() => {
84
92
  symbols();
85
93
  edits();
@@ -90,7 +98,6 @@ export class FoldingFeature {
90
98
  }
91
99
 
92
100
  private publish(): void {
93
- const hints: EditorFoldHints = { symbols: this.symbols, ranges: this.ranges };
94
- this.language.editor.setFoldHints(hints);
101
+ this.language.editor.setFoldHints({ symbols: this.symbols, ranges: this.ranges });
95
102
  }
96
103
  }
@@ -4,40 +4,42 @@ import { comparePositions } from './diagnostics-model.ts';
4
4
  import type { EditorLanguage } from './editor-language.ts';
5
5
 
6
6
  const METHOD = 'textDocument/onTypeFormatting';
7
+
7
8
  /* A line break with the indentation the editor put after it, which is how Enter reaches the server. */
8
9
  const LINE_BREAK = /^\r?\n[ \t]*(\r?\n[ \t]*)?$/;
10
+
9
11
  /* A closer with the whitespace the editor replaced in front of it when it moved the closer back to its opener's indentation. */
10
12
  const DEDENTED = /^[ \t]+(\S)$/u;
11
13
 
12
14
  /* The overlapping edits of a server answer have no single meaning, so an answer that has them is not applied. */
13
15
  function overlaps(edits: readonly TextEdit[]): boolean {
14
16
  const ordered = [...edits].sort((left, right) => comparePositions(left.range.start, right.range.start));
17
+
15
18
  return ordered.some((edit, index) => index > 0 && comparePositions(ordered[index - 1]!.range.end, edit.range.start) > 0);
16
19
  }
17
20
 
18
21
  /*
19
- * Formatting while typing: after a person types one of the characters the server named in
20
- * `firstTriggerCharacter` and `moreTriggerCharacter`, the server is asked what to change around the caret, and its
21
- * edits are applied.
22
+ * Formatting while typing: after one of the server's trigger characters is typed, the server is asked what
23
+ * to change around the caret, and its edits join the undo step of the typed character.
22
24
  *
23
- * Only typing counts: one change that is the trigger character alone, the closer the editor moved back to its
24
- * opener's indentation, or a line break with its indentation for `\n`, which the editor reports as typed though
25
- * Enter runs as a command. A character the editor types over an existing closer changes no text and asks nothing. A paste, undo, a completion or an external change does not ask, and neither does a
26
- * second caret, a read-only editor or a snippet session, whose tab stops an edit could move. An answer is applied
27
- * only while the text is exactly what it was asked about, and the caret where it was.
28
- *
29
- * The edits join the undo step of the typed character, so one undo takes both back. Off by default; a host enables it explicitly with `configure(true)`.
25
+ * Only typing counts: the trigger character alone, the closer the editor moved back to its opener's
26
+ * indentation, or a line break with its indentation for `\n`. A paste, undo, a completion, an external change,
27
+ * a second caret, a read-only editor or a snippet session (whose tab stops an edit could move) asks nothing.
28
+ * An answer is applied only while the text and the caret are what it was asked about. Off until `configure(true)`.
30
29
  */
31
30
  export class FormatOnTypeFeature {
32
31
  private readonly language: EditorLanguage;
33
32
  private enabled = false;
33
+
34
34
  /* Raised by every change of the text, so an answer for an older text is told from the one for the text that is. */
35
35
  private revision = 0;
36
+
36
37
  private controller = new AbortController();
37
38
 
38
39
  constructor(language: EditorLanguage) {
39
40
  this.language = language;
40
41
  const off = language.editor.onTextChange((change) => this.typed(change));
42
+
41
43
  language.onDispose(() => {
42
44
  off();
43
45
  this.controller.abort();
@@ -51,11 +53,13 @@ export class FormatOnTypeFeature {
51
53
  /* Whether the server of this document formats on type and the service can ask it. */
52
54
  get supported(): boolean {
53
55
  const { project, uri } = this.language;
56
+
54
57
  return project.service.onTypeFormatting !== undefined && project.service.supports(METHOD, uri);
55
58
  }
56
59
 
57
60
  configure(enabled: boolean): void {
58
61
  this.enabled = enabled;
62
+
59
63
  if (!enabled) {
60
64
  this.controller.abort();
61
65
  }
@@ -64,6 +68,7 @@ export class FormatOnTypeFeature {
64
68
  /* The characters that ask the server, the first one and the more. */
65
69
  triggerCharacters(): readonly string[] {
66
70
  const options = this.language.project.service.providerOptions(METHOD, this.language.uri);
71
+
67
72
  return options === undefined
68
73
  ? []
69
74
  : [...(options.firstTriggerCharacter === undefined ? [] : [options.firstTriggerCharacter]), ...(options.moreTriggerCharacter ?? [])];
@@ -72,33 +77,41 @@ export class FormatOnTypeFeature {
72
77
  private typed(change: EditorTextChange): void {
73
78
  const revision = ++this.revision;
74
79
  this.controller.abort();
80
+
75
81
  if (!this.enabled || (change.source !== 'input' && change.typed !== true) || change.changes.length !== 1 || !this.supported) {
76
82
  return;
77
83
  }
84
+
78
85
  const { editor, snippets } = this.language;
79
86
  const { text } = change.changes[0]!;
80
87
  const typed = LINE_BREAK.test(text) ? '\n' : Array.from(text).length === 1 ? text : (DEDENTED.exec(text)?.[1] ?? null);
88
+
81
89
  if (typed === null || !this.triggerCharacters().includes(typed)) {
82
90
  return;
83
91
  }
92
+
84
93
  if (snippets.isActive || editor.isReadOnly?.() === true || editor.getSelections().length !== 1) {
85
94
  return;
86
95
  }
96
+
87
97
  this.controller = new AbortController();
88
98
  void this.ask(revision, typed, editor.getCaret(), this.controller.signal);
89
99
  }
90
100
 
91
101
  private async ask(revision: number, typed: string, caret: { line: number; character: number }, signal: AbortSignal): Promise<void> {
92
102
  const { editor, project, uri, document } = this.language;
103
+
93
104
  try {
94
105
  // The change of the typed character reaches the server before this question does.
95
106
  await document.ready;
96
107
  const edits = await project.service.onTypeFormatting!(uri, caret, typed, { ...editor.getIndentation() }, { signal });
97
108
  const here = editor.getCaret();
98
109
  const stale = revision !== this.revision || this.language.isDisposed || comparePositions(here, caret) !== 0;
110
+
99
111
  if (stale || edits === null || edits.length === 0 || overlaps(edits)) {
100
112
  return;
101
113
  }
114
+
102
115
  editor.applyEdits(
103
116
  edits.map((edit) => ({ range: edit.range, text: edit.newText })),
104
117
  { mergeUndo: true }
@@ -21,6 +21,7 @@ export class HighlightLayers {
21
21
  } else {
22
22
  this.owners.set(owner, provider);
23
23
  }
24
+
24
25
  this.editor.setLineHighlights([...this.owners.values()].flatMap((read) => read()));
25
26
  }
26
27
  }
@@ -29,9 +30,11 @@ const layers = new WeakMap<Editor, HighlightLayers>();
29
30
 
30
31
  export function highlightLayers(editor: Editor): HighlightLayers {
31
32
  let found = layers.get(editor);
33
+
32
34
  if (found === undefined) {
33
35
  found = new HighlightLayers(editor);
34
36
  layers.set(editor, found);
35
37
  }
38
+
36
39
  return found;
37
40
  }
package/src/highlights.ts CHANGED
@@ -25,10 +25,13 @@ export function neighborRange(ranges: readonly EditorRange[], from: EditorPositi
25
25
  if (ranges.length === 0) {
26
26
  return null;
27
27
  }
28
+
28
29
  const ordered = [...ranges].sort((left, right) => comparePositions(left.start, right.start));
30
+
29
31
  if (direction === 1) {
30
32
  return ordered.find((range) => comparePositions(range.start, from) > 0) ?? ordered[0]!;
31
33
  }
34
+
32
35
  return [...ordered].reverse().find((range) => comparePositions(range.start, from) < 0) ?? ordered[ordered.length - 1]!;
33
36
  }
34
37
 
@@ -44,13 +47,17 @@ export class HighlightsFeature {
44
47
  constructor(language: EditorLanguage, timers: Timers = realTimers) {
45
48
  this.language = language;
46
49
  const { editor, project, uri } = language;
50
+
47
51
  this.refresher = new Refresher(
48
52
  async (signal) => {
49
53
  if (!project.service.supports(METHOD, uri) || !this.onName()) {
50
54
  this.clear();
55
+
51
56
  return;
52
57
  }
58
+
53
59
  const result = await project.service.documentHighlights(uri, editor.getCaret(), { signal });
60
+
54
61
  if (!signal.aborted) {
55
62
  this.set(result === null ? [] : editorHighlightsOf(result));
56
63
  }
@@ -58,12 +65,14 @@ export class HighlightsFeature {
58
65
  PAUSE_MS,
59
66
  timers
60
67
  );
68
+
61
69
  const offs = [
62
70
  editor.onCaret((position) => {
63
71
  // Moving along the same name keeps its marks, which is what makes stepping through them quiet.
64
72
  if (!this.current.some((highlight) => rangeHolds(highlight.range, position))) {
65
73
  this.clear();
66
74
  }
75
+
67
76
  this.refresher.later();
68
77
  }),
69
78
  editor.onTextChange(() => {
@@ -73,16 +82,20 @@ export class HighlightsFeature {
73
82
  }),
74
83
  language.onKey((event) => {
75
84
  const next = isShortcut(this.language.shortcuts.nextHighlight, event);
85
+
76
86
  if (!next && !isShortcut(this.language.shortcuts.previousHighlight, event)) {
77
87
  return false;
78
88
  }
89
+
79
90
  return this.step(next ? 1 : -1);
80
91
  }, KEY_PRIORITY.highlights)
81
92
  ];
93
+
82
94
  language.onDispose(() => {
83
95
  for (const off of offs) {
84
96
  off();
85
97
  }
98
+
86
99
  this.refresher.dispose();
87
100
  editor.setHighlights([]);
88
101
  });
@@ -91,15 +104,19 @@ export class HighlightsFeature {
91
104
  /* Puts the caret on the next use of the name, or the one before; false when there are none. */
92
105
  step(direction: 1 | -1): boolean {
93
106
  const { editor } = this.language;
107
+
94
108
  const target = neighborRange(
95
109
  this.current.map((highlight) => highlight.range),
96
110
  editor.getCaret(),
97
111
  direction
98
112
  );
113
+
99
114
  if (target === null) {
100
115
  return false;
101
116
  }
117
+
102
118
  editor.setCaret(target.start, comparePositions(target.start, editor.getCaret()) > 0 ? 'centerDown' : 'centerUp');
119
+
103
120
  return true;
104
121
  }
105
122
 
@@ -108,6 +125,7 @@ export class HighlightsFeature {
108
125
  const caret = editor.getCaret();
109
126
  const before = caret.character > 0 ? editor.textInRange({ start: { line: caret.line, character: caret.character - 1 }, end: caret }) : '';
110
127
  const after = editor.textInRange({ start: caret, end: { line: caret.line, character: caret.character + 1 } });
128
+
111
129
  return (before !== '' && isIdentifierCharacter(before)) || (after !== '' && isIdentifierCharacter(after));
112
130
  }
113
131