@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
@@ -1,18 +1,23 @@
1
1
  import { splitLines } from '@adecore/merge';
2
2
  import type { GitBlameCommit, GitBlameResult } from './host-types.ts';
3
3
  import type { EditorCodeVision, EditorCodeVisionEntry, EditorRect } from '@adecore/editor';
4
- import type { DocumentSymbolResult } from '@adecore/lsp';
4
+ import type { DocumentSymbolResult, Location } from '@adecore/lsp';
5
5
  import type { EditorLanguage } from './editor-language.ts';
6
6
  import { type CodeAuthorship, authorsText, authorshipOf, mapBlame } from './code-authors.ts';
7
7
  import { type CodeVisionDeclaration, declarationsOf, usagesText } from './code-vision-model.ts';
8
8
  import { realTimers, type Timers } from './timers.ts';
9
+ import { placeKey } from './unique.ts';
9
10
 
10
11
  const REFERENCES = 'textDocument/references';
12
+
11
13
  /* Lines asked about above and below what is in view, so a little scrolling stays inside what is counted. */
12
14
  const MARGIN_LINES = 40;
15
+
13
16
  /* References are the slowest question a server is asked here, so only this many are in flight at once. */
14
17
  const MAX_PARALLEL = 3;
18
+
15
19
  const SCROLL_PAUSE_MS = 150;
20
+
16
21
  /* Past this many lines a file gets no rows at all, which is also what keeps blame and the walk of symbols cheap. */
17
22
  export const MAX_LINES = 20_000;
18
23
 
@@ -40,18 +45,25 @@ export class CodeVisionFeature {
40
45
  private readonly timers: Timers;
41
46
  private settings: CodeVisionSettings = { usages: false, authors: false };
42
47
  private declarations: CodeVisionDeclaration[] = [];
48
+
43
49
  /* What the declarations were read from, so a count never outlives the text it was asked for. */
44
50
  private epoch = 0;
51
+
45
52
  private edited = false;
46
53
  private readonly counts = new Map<string, { count: number; epoch: number }>();
47
54
  private queue: CodeVisionDeclaration[] = [];
55
+
48
56
  /* Scrolling changes the pending queue but keeps requests already in flight. */
49
57
  private readonly running = new Map<string, AbortController>();
58
+
50
59
  /* Declarations a server could not count for this text, which are not asked again until it says it can. */
51
60
  private failed = new Set<string>();
61
+
52
62
  private blame: BlameSource | null = null;
63
+
53
64
  /* The text the declarations were read from, which the lines of their authors are counted in. */
54
65
  private lines: readonly string[] = [];
66
+
55
67
  private mapped: { blame: GitBlameResult; lines: Int32Array | null } | null = null;
56
68
  private readonly authorships = new Map<string, CodeAuthorship>();
57
69
  private scrollTimer: unknown;
@@ -62,6 +74,7 @@ export class CodeVisionFeature {
62
74
  this.language = language;
63
75
  this.timers = timers;
64
76
  const { editor } = language;
77
+
65
78
  const offs = [
66
79
  onSymbols((result) => this.read(result)),
67
80
  editor.onTextChange(() => {
@@ -73,21 +86,27 @@ export class CodeVisionFeature {
73
86
  this.scrollTimer = this.timers.set(() => this.plan(), SCROLL_PAUSE_MS);
74
87
  })
75
88
  ];
89
+
76
90
  const providers = language.onProvidersChanged(() => {
77
91
  this.failed.clear();
78
92
  this.publish();
79
93
  this.plan();
80
94
  });
95
+
81
96
  language.onDispose(() => {
82
97
  this.disposed = true;
98
+
83
99
  for (const off of offs) {
84
100
  off();
85
101
  }
102
+
86
103
  providers();
87
104
  this.timers.clear(this.scrollTimer);
105
+
88
106
  for (const controller of this.running.values()) {
89
107
  controller.abort();
90
108
  }
109
+
91
110
  this.closeAuthors();
92
111
  editor.setCodeVision([]);
93
112
  });
@@ -98,6 +117,7 @@ export class CodeVisionFeature {
98
117
  if (settings.usages === this.settings.usages && settings.authors === this.settings.authors) {
99
118
  return;
100
119
  }
120
+
101
121
  this.settings = settings;
102
122
  this.publish();
103
123
  this.plan();
@@ -118,8 +138,10 @@ export class CodeVisionFeature {
118
138
  this.mapped = null;
119
139
  this.authorships.clear();
120
140
  this.edited = false;
141
+
121
142
  this.declarations =
122
143
  lines.length > MAX_LINES ? [] : declarationsOf(result, { lineCount: lines.length, lineAt: (line) => lines[line] ?? '', languageId });
144
+
123
145
  this.epoch++;
124
146
  this.failed.clear();
125
147
  this.publish();
@@ -135,9 +157,11 @@ export class CodeVisionFeature {
135
157
  if (this.disposed || this.edited || !this.canCount) {
136
158
  return;
137
159
  }
160
+
138
161
  const view = this.language.editor.getVisibleRange();
139
162
  const first = view.start.line - MARGIN_LINES;
140
163
  const last = view.end.line + MARGIN_LINES;
164
+
141
165
  this.queue = this.declarations
142
166
  .filter(
143
167
  (declaration) =>
@@ -149,6 +173,7 @@ export class CodeVisionFeature {
149
173
  !this.failed.has(declaration.id)
150
174
  )
151
175
  .sort((left, right) => Math.abs(left.line - view.start.line) - Math.abs(right.line - view.start.line));
176
+
152
177
  while (this.running.size < MAX_PARALLEL && this.queue.length > 0) {
153
178
  void this.count(this.queue.shift()!);
154
179
  }
@@ -159,8 +184,10 @@ export class CodeVisionFeature {
159
184
  const controller = new AbortController();
160
185
  this.running.set(declaration.id, controller);
161
186
  const epoch = this.epoch;
187
+
162
188
  try {
163
189
  const locations = await project.service.references(uri, declaration.position, false, { signal: controller.signal, parallel: true });
190
+
164
191
  if (!this.disposed && epoch === this.epoch) {
165
192
  this.counts.set(declaration.id, { count: distinctPlaces(locations ?? []), epoch });
166
193
  this.publish();
@@ -181,9 +208,12 @@ export class CodeVisionFeature {
181
208
  if (this.publishing) {
182
209
  return;
183
210
  }
211
+
184
212
  this.publishing = true;
213
+
185
214
  queueMicrotask(() => {
186
215
  this.publishing = false;
216
+
187
217
  if (!this.disposed) {
188
218
  this.language.editor.setCodeVision(this.rows());
189
219
  }
@@ -197,14 +227,18 @@ export class CodeVisionFeature {
197
227
  const withAuthors = this.settings.authors && this.blame !== null;
198
228
  const commits = this.blame?.kind === 'ready' ? this.blame.blame.commits : [];
199
229
  const mapped = this.blame?.kind === 'ready' && this.settings.authors ? this.mapOfBlame(this.blame) : null;
230
+
200
231
  for (const declaration of this.declarations) {
201
232
  const reserved = (counted && declaration.usages) || (withAuthors && declaration.authors);
233
+
202
234
  if (!reserved || taken.has(declaration.line)) {
203
235
  continue;
204
236
  }
237
+
205
238
  taken.add(declaration.line);
206
239
  const entries: EditorCodeVisionEntry[] = [];
207
240
  const known = this.counts.get(declaration.id);
241
+
208
242
  if (counted && declaration.usages && known !== undefined) {
209
243
  entries.push({
210
244
  id: 'usages',
@@ -212,8 +246,10 @@ export class CodeVisionFeature {
212
246
  activate: () => void this.language.peek.open(declaration.position)
213
247
  });
214
248
  }
249
+
215
250
  if (mapped !== null && declaration.authors) {
216
251
  const authorship = this.authorshipOf(declaration, commits, mapped);
252
+
217
253
  entries.push({
218
254
  id: 'authors',
219
255
  text: authorsText(authorship, this.language.project.i18n),
@@ -221,18 +257,23 @@ export class CodeVisionFeature {
221
257
  activate: (anchor) => this.showAuthors(declaration, authorship, anchor)
222
258
  });
223
259
  }
260
+
224
261
  rows.push({ id: declaration.id, line: declaration.line, entries });
225
262
  }
263
+
226
264
  return rows;
227
265
  }
228
266
 
229
267
  private authorshipOf(declaration: CodeVisionDeclaration, commits: readonly GitBlameCommit[], mapped: Int32Array): CodeAuthorship {
230
268
  const known = this.authorships.get(declaration.id);
269
+
231
270
  if (known !== undefined) {
232
271
  return known;
233
272
  }
273
+
234
274
  const authorship = authorshipOf(commits, mapped, this.lines, declaration.authorFrom, declaration.authorTo);
235
275
  this.authorships.set(declaration.id, authorship);
276
+
236
277
  return authorship;
237
278
  }
238
279
 
@@ -240,12 +281,14 @@ export class CodeVisionFeature {
240
281
  if (this.mapped?.blame !== source.blame) {
241
282
  this.mapped = { blame: source.blame, lines: mapBlame(source.blame, source.base, this.lines) };
242
283
  }
284
+
243
285
  return this.mapped.lines;
244
286
  }
245
287
 
246
288
  private showAuthors(declaration: CodeVisionDeclaration, authorship: CodeAuthorship, anchor: EditorRect): void {
247
289
  const source = this.blame;
248
290
  const openCommit = source?.kind === 'ready' ? source.openCommit : undefined;
291
+
249
292
  this.language.popups.setState({
250
293
  authors: {
251
294
  anchor,
@@ -260,6 +303,7 @@ export class CodeVisionFeature {
260
303
  closeAuthors(restoreFocus = false): void {
261
304
  if (this.language.popups.getState().authors !== null) {
262
305
  this.language.popups.setState({ authors: null });
306
+
263
307
  if (restoreFocus) {
264
308
  this.language.editor.focus();
265
309
  }
@@ -268,6 +312,6 @@ export class CodeVisionFeature {
268
312
  }
269
313
 
270
314
  /* The places a server named, each counted once however many ways it was reached. */
271
- function distinctPlaces(locations: readonly { uri: string; range: { start: { line: number; character: number } } }[]): number {
272
- return new Set(locations.map((location) => `${location.uri}\0${location.range.start.line}\0${location.range.start.character}`)).size;
315
+ function distinctPlaces(locations: readonly Location[]): number {
316
+ return new Set(locations.map(placeKey)).size;
273
317
  }
@@ -43,10 +43,13 @@ export function isCallItem(item: CompletionItem, lineBefore: string): boolean {
43
43
  function listedParameters(signature: string): boolean | null {
44
44
  const text = signature.replace(SIGNATURE_KIND_PREFIX, '');
45
45
  const open = text.indexOf('(');
46
+
46
47
  if (open < 0) {
47
48
  return null;
48
49
  }
50
+
49
51
  let depth = 0;
52
+
50
53
  for (let i = open; i < text.length; i++) {
51
54
  if (text[i] === '(') {
52
55
  depth++;
@@ -54,24 +57,29 @@ function listedParameters(signature: string): boolean | null {
54
57
  return text.slice(open + 1, i).trim() !== '';
55
58
  }
56
59
  }
60
+
57
61
  return null;
58
62
  }
59
63
 
60
64
  /* From what the server says of the item: `(method) Box.open(flag: boolean): void` has parameters and `(): void` has none. Null when it says nothing about them. */
61
65
  export function hasParameters(item: CompletionItem): boolean | null {
62
66
  const lines = (item.detail ?? '').split('\n').filter((line) => !SOURCE_LINE.test(line.trim()));
67
+
63
68
  for (const signature of [item.labelDetails?.detail ?? '', ...lines]) {
64
69
  const listed = listedParameters(signature);
70
+
65
71
  if (listed !== null) {
66
72
  return listed;
67
73
  }
68
74
  }
75
+
69
76
  return null;
70
77
  }
71
78
 
72
79
  /* The text before the innermost brace that is still open, which says whether the caret is in an import or export list. */
73
80
  function beforeOpenBrace(before: string): string | null {
74
81
  let depth = 0;
82
+
75
83
  for (let i = before.length - 1; i >= 0; i--) {
76
84
  if (before[i] === '}') {
77
85
  depth++;
@@ -79,6 +87,7 @@ function beforeOpenBrace(before: string): string | null {
79
87
  return before.slice(0, i);
80
88
  }
81
89
  }
90
+
82
91
  return null;
83
92
  }
84
93
 
@@ -86,12 +95,15 @@ function beforeOpenBrace(before: string): string | null {
86
95
  function isNameOnly(languageId: string, before: string): boolean {
87
96
  const lineBefore = before.slice(before.lastIndexOf('\n') + 1);
88
97
  const brace = beforeOpenBrace(before);
98
+
89
99
  if (brace !== null && (IMPORT_BRACE.test(brace) || (languageId === 'php' && PHP_USE_BRACE.test(brace)))) {
90
100
  return true;
91
101
  }
102
+
92
103
  if (TYPE_OPERATOR.test(lineBefore) || /^\s*export\s+default\s+[\w$]*$/.test(lineBefore)) {
93
104
  return true;
94
105
  }
106
+
95
107
  switch (languageId) {
96
108
  case 'php':
97
109
  return /^\s*use\s/.test(lineBefore) || /#\[[^\]]*$/.test(lineBefore);
@@ -111,19 +123,25 @@ function isNameOnly(languageId: string, before: string): boolean {
111
123
  export function planCall(site: CallSite): CallPlan {
112
124
  const { item, languageId, insertion, before, rest, replace } = site;
113
125
  const lineBefore = before.slice(before.lastIndexOf('\n') + 1);
126
+
114
127
  if (!CALL_LANGUAGES.has(languageId) || !isCallItem(item, lineBefore) || item.insertTextFormat === SNIPPET_FORMAT || insertion.text.includes('(')) {
115
128
  return { action: 'none' };
116
129
  }
130
+
117
131
  if (isNameOnly(languageId, before)) {
118
132
  return { action: 'none' };
119
133
  }
134
+
120
135
  const existing = EXISTING_CALL.exec(rest);
136
+
121
137
  if (existing !== null) {
122
138
  return replace ? { action: 'enter', shift: existing[0].length } : { action: 'none' };
123
139
  }
140
+
124
141
  if (rest.startsWith('<')) {
125
142
  return { action: 'none' };
126
143
  }
144
+
127
145
  return { action: 'add', inside: hasParameters(item) !== false };
128
146
  }
129
147
 
@@ -131,5 +149,6 @@ export function planCall(site: CallSite): CallPlan {
131
149
  export function withParentheses(insertion: Insertion, inside: boolean): Insertion {
132
150
  const text = `${insertion.text}()`;
133
151
  const caret = inside ? text.length - 1 : text.length;
152
+
134
153
  return { range: insertion.range, text, stops: [{ index: 0, start: caret, end: caret }] };
135
154
  }
@@ -2,7 +2,7 @@ import { labelCollator } from '@adecore/ui/format';
2
2
  import type { SnippetTransformJob } from './snippet-transform-protocol.ts';
3
3
  import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
4
4
  import { applyCompletionDefaults, type CompletionItem, type CompletionResult, type MarkupContent } from '@adecore/lsp';
5
- import { comparePositions } from './diagnostics-model.ts';
5
+ import { comparePositions, sameRange } from './diagnostics-model.ts';
6
6
  import { escapeMarkdown, isEmptyHover, splitSignatures, type HoverText } from './hover-content.ts';
7
7
  import type { CompletionDocs } from './popups.ts';
8
8
  import { compileSnippet, parseSnippet, tabOrder, type SnippetField, type SnippetStop, type SnippetTemplate, type SnippetVariableResolver } from './snippet.ts';
@@ -22,18 +22,23 @@ export function identifierPrefix(before: string): string {
22
22
  if (before === lastBefore) {
23
23
  return lastPrefix;
24
24
  }
25
+
25
26
  const characters = Array.from(before);
26
27
  let start = characters.length;
28
+
27
29
  while (start > 0 && IDENTIFIER_CHARACTER.test(characters[start - 1]!)) {
28
30
  start--;
29
31
  }
32
+
30
33
  lastBefore = before;
31
34
  lastPrefix = characters.slice(start).join('');
35
+
32
36
  return lastPrefix;
33
37
  }
34
38
 
35
39
  const OPENS_VALUE =
36
40
  /(?:[-=!<>&|?+*/%^:,]|(?:^|[^\p{L}\p{N}_$])(?:return|new|case|in|of|instanceof|typeof|keyof|await|yield|throw|extends|implements|as|is|not|and|or|echo|print|use|import|from|clone))$/u;
41
+
37
42
  const NOT_A_VALUE = /(?:\+\+|--|\/\/|\/\*\*?|^\s*[-+*])$/;
38
43
 
39
44
  /*
@@ -45,6 +50,7 @@ export function opensCompletion(before: string, text: string): boolean {
45
50
  if (text === '(' || text === '()' || text === '[' || text === '[]') {
46
51
  return true;
47
52
  }
53
+
48
54
  return text === ' ' && OPENS_VALUE.test(before) && !NOT_A_VALUE.test(before);
49
55
  }
50
56
 
@@ -73,10 +79,13 @@ const shapes = new Map<string, LabelShape>();
73
79
  /* What matching reads of a label, which is the same at every keystroke of a word and costs a regular expression pass to find. */
74
80
  function shapeOf(value: string): LabelShape {
75
81
  const known = shapes.get(value);
82
+
76
83
  if (known !== undefined) {
77
84
  return known;
78
85
  }
86
+
79
87
  const boundaries = [...value.matchAll(/(?:^|[_$\s-])([\p{L}\p{N}])|(\p{Lu})/gu)];
88
+
80
89
  const shape: LabelShape = {
81
90
  lower: value.toLocaleLowerCase(),
82
91
  offsets: boundaries.map((boundary) => boundary.index! + (boundary[1] ? boundary[0].length - boundary[1].length : 0)),
@@ -85,10 +94,13 @@ function shapeOf(value: string): LabelShape {
85
94
  .join('')
86
95
  .toLocaleLowerCase()
87
96
  };
97
+
88
98
  if (shapes.size >= SHAPE_LIMIT) {
89
99
  shapes.clear();
90
100
  }
101
+
91
102
  shapes.set(value, shape);
103
+
92
104
  return shape;
93
105
  }
94
106
 
@@ -102,35 +114,46 @@ export function matchDetail(value: string, prefix: string): MatchDetail | undefi
102
114
  if (prefix === '') {
103
115
  return { score: 0, positions: [] };
104
116
  }
117
+
105
118
  if (value.startsWith(prefix)) {
106
119
  return { score: 0, positions: run(0, prefix.length) };
107
120
  }
121
+
108
122
  const { lower, offsets, initials } = shapeOf(value);
109
123
  const query = prefix.toLocaleLowerCase();
124
+
110
125
  if (lower.startsWith(query)) {
111
126
  return { score: 1, positions: run(0, query.length) };
112
127
  }
128
+
113
129
  for (const offset of offsets) {
114
130
  if (lower.slice(offset).startsWith(query)) {
115
131
  return { score: 2 + offset / 1000, positions: run(offset, query.length) };
116
132
  }
117
133
  }
134
+
118
135
  if (initials.startsWith(query)) {
119
136
  return { score: 3, positions: offsets.slice(0, query.length) };
120
137
  }
138
+
121
139
  if (query.length < 2 || lower[0] !== query[0]) {
122
140
  return undefined;
123
141
  }
142
+
124
143
  let cursor = 0;
125
144
  const positions: number[] = [];
145
+
126
146
  for (const character of query) {
127
147
  const next = lower.indexOf(character, cursor);
148
+
128
149
  if (next < 0) {
129
150
  return undefined;
130
151
  }
152
+
131
153
  positions.push(next);
132
154
  cursor = next + character.length;
133
155
  }
156
+
134
157
  return { score: 4 + cursor / 1000, positions };
135
158
  }
136
159
 
@@ -141,12 +164,15 @@ export function matchScore(value: string, prefix: string): number | undefined {
141
164
  /* The range an item replaces when it is accepted; for an insert-or-replace edit, the one `replace` asks for. */
142
165
  export function editRangeOf(item: CompletionItem, replace: boolean): EditorRange | null {
143
166
  const edit = item.textEdit;
167
+
144
168
  if (edit === undefined) {
145
169
  return null;
146
170
  }
171
+
147
172
  if ('range' in edit) {
148
173
  return edit.range;
149
174
  }
175
+
150
176
  return replace ? edit.replace : edit.insert;
151
177
  }
152
178
 
@@ -154,18 +180,21 @@ export function editRangeOf(item: CompletionItem, replace: boolean): EditorRange
154
180
  export function itemsOf(result: CompletionResult): { items: CompletionItem[]; incomplete: boolean } {
155
181
  const list = result === null || Array.isArray(result) ? undefined : result;
156
182
  const items = result === null ? [] : Array.isArray(result) ? result : result.items;
183
+
157
184
  return {
158
185
  incomplete: list?.isIncomplete ?? false,
159
186
  items: items.flatMap((item) => {
160
187
  if (typeof item?.label !== 'string') {
161
188
  return [];
162
189
  }
190
+
163
191
  try {
164
192
  return [applyCompletionDefaults(item, list?.itemDefaults, { onInvalidRange: () => undefined })];
165
193
  } catch (error) {
166
194
  if (error instanceof TypeError || error instanceof RangeError) {
167
195
  return [];
168
196
  }
197
+
169
198
  throw error;
170
199
  }
171
200
  })
@@ -179,9 +208,11 @@ export function itemsOf(result: CompletionResult): { items: CompletionItem[]; in
179
208
  */
180
209
  export function prefixFor(item: CompletionItem, caret: EditorPosition, lineBefore: string, textBetween: (range: EditorRange) => string): string {
181
210
  const range = editRangeOf(item, false);
211
+
182
212
  if (range !== null && range.start.line === caret.line && comparePositions(range.start, caret) <= 0 && comparePositions(caret, range.end) <= 0) {
183
213
  return textBetween({ start: range.start, end: caret });
184
214
  }
215
+
185
216
  return identifierPrefix(lineBefore);
186
217
  }
187
218
 
@@ -203,9 +234,11 @@ export function rankCompletions(
203
234
  limit = 150
204
235
  ): Ranked[] {
205
236
  const { compare } = labelCollator();
237
+
206
238
  return items
207
239
  .map((item, index) => {
208
240
  const prefix = prefixOf(item);
241
+
209
242
  return { item, index, prefix, score: matchScore(item.filterText ?? item.label, prefix), recent: recency(item) };
210
243
  })
211
244
  .filter((entry): entry is typeof entry & { score: number } => entry.score !== undefined)
@@ -230,12 +263,14 @@ export function matchedCharacters(label: string, prefix: string): readonly numbe
230
263
  function sourceOf(item: CompletionItem): string {
231
264
  const detail = item.detail?.split('\n')[0]?.trim() ?? '';
232
265
  const imported = /^use\s+(.+)$/.exec(detail);
266
+
233
267
  return imported === null ? detail : imported[1]!.slice(0, Math.max(0, imported[1]!.lastIndexOf('\\')));
234
268
  }
235
269
 
236
270
  /* What stands right-aligned after a row: the module or namespace the item comes from, else what the server calls its type. */
237
271
  export function qualifierOf(item: CompletionItem): string {
238
272
  const description = item.labelDetails?.description?.trim() ?? '';
273
+
239
274
  return description !== '' ? description : sourceOf(item);
240
275
  }
241
276
 
@@ -247,16 +282,20 @@ export function qualifiersOf(items: readonly CompletionItem[]): string[] {
247
282
  const result = items.map(qualifierOf);
248
283
  const groups = new Map<string, number[]>();
249
284
  items.forEach((item, index) => groups.set(item.label, [...(groups.get(item.label) ?? []), index]));
285
+
250
286
  for (const indexes of groups.values()) {
251
287
  if (indexes.length < 2 || new Set(indexes.map((index) => result[index])).size === indexes.length) {
252
288
  continue;
253
289
  }
290
+
254
291
  const alternate = indexes.map((index) => sourceOf(items[index]!) || result[index]!);
255
292
  const distinct = new Set(alternate).size === indexes.length && !alternate.includes('');
293
+
256
294
  indexes.forEach((index, position) => {
257
295
  result[index] = distinct ? alternate[position]! : `${result[index] ?? ''} #${position + 1}`.trim();
258
296
  });
259
297
  }
298
+
260
299
  return result;
261
300
  }
262
301
 
@@ -278,6 +317,7 @@ export function isCallableKind(kind: number | undefined): boolean {
278
317
  /* A call snippet with the call cut off, for a name that already has its parentheses: `log(${1:value})$0` becomes `log`. */
279
318
  function withoutCall(snippet: string): string {
280
319
  const open = snippet.search(/(?<!\\)\(/);
320
+
281
321
  return open > 0 ? snippet.slice(0, open) : snippet;
282
322
  }
283
323
 
@@ -313,23 +353,28 @@ export function insertionOf(
313
353
  ): Insertion {
314
354
  const raw = item.textEdit?.newText ?? item.insertText ?? item.label;
315
355
  const edit = editRangeOf(item, replace);
356
+
316
357
  const range =
317
358
  edit === null
318
359
  ? { start: { line: caret.line, character: caret.character - identifierPrefix(lineBefore).length }, end: caret }
319
360
  : // The word grew since the answer came, so what was typed since belongs to what is replaced.
320
361
  { start: edit.start, end: comparePositions(edit.end, caret) < 0 && edit.end.line === caret.line ? caret : edit.end };
362
+
321
363
  if (item.insertTextFormat !== SNIPPET_FORMAT) {
322
364
  const adjusted =
323
365
  item.insertTextMode === 2
324
366
  ? indentSnippet({ text: raw, stops: [] }, options.indentationBefore ?? lineBefore.slice(0, range.start.character))
325
367
  : { text: raw, stops: [] };
368
+
326
369
  return { range, text: adjusted.text, stops: [], ...(adjusted.limited ? { limited: true } : {}) };
327
370
  }
371
+
328
372
  const followed = range.end.line === caret.line && lineAfter.slice(Math.max(0, range.end.character - caret.character)).startsWith('(');
329
373
  const source = followed && isCallableKind(item.kind) ? withoutCall(raw) : raw;
330
374
  const template = options.template?.source === source ? options.template : compileSnippet(source);
331
375
  const rendered = template.render(options.resolve, options.variableTransforms);
332
376
  const parsed = item.insertTextMode === 2 ? indentSnippet(rendered, options.indentationBefore ?? lineBefore.slice(0, range.start.character)) : rendered;
377
+
333
378
  return {
334
379
  range,
335
380
  text: parsed.text,
@@ -358,23 +403,30 @@ export function mirroredInsertions(
358
403
  ): EditorContentChange[] {
359
404
  const before = insertion.range.start.line === caret.line ? caret.character - insertion.range.start.character : -1;
360
405
  const after = insertion.range.end.line === caret.line ? insertion.range.end.character - caret.character : -1;
406
+
361
407
  if (before < 0 || after < 0) {
362
408
  return [];
363
409
  }
410
+
364
411
  const typed = text({ start: insertion.range.start, end: caret });
365
412
  const replaced = text({ start: caret, end: insertion.range.end });
413
+
366
414
  return carets.flatMap((other): EditorContentChange[] => {
367
415
  const at = includeSelections ? other.end : other.start;
368
- if (comparePositions(other.start, primary.start) === 0 && comparePositions(other.end, primary.end) === 0) {
416
+
417
+ if (sameRange(other, primary)) {
369
418
  return [];
370
419
  }
420
+
371
421
  if ((!includeSelections && comparePositions(other.start, other.end) !== 0) || at.character < before) {
372
422
  return [];
373
423
  }
424
+
374
425
  const range = { start: { line: at.line, character: at.character - before }, end: { line: at.line, character: at.character + after } };
375
426
  const preceding = range.start.character > 0 ? text({ start: { line: at.line, character: range.start.character - 1 }, end: range.start }) : '';
376
427
  const startsWord = preceding === '' || !isIdentifierCharacter(preceding);
377
428
  const matches = startsWord && text({ start: range.start, end: at }) === typed && text({ start: at, end: range.end }) === replaced;
429
+
378
430
  return matches ? [{ range, text: insertion.text }] : [];
379
431
  });
380
432
  }
@@ -383,6 +435,7 @@ export function documentationText(documentation: string | MarkupContent | undefi
383
435
  if (documentation === undefined) {
384
436
  return '';
385
437
  }
438
+
386
439
  return typeof documentation === 'string' ? documentation : documentation.kind === 'plaintext' ? escapeMarkdown(documentation.value) : documentation.value;
387
440
  }
388
441
 
@@ -400,10 +453,12 @@ export function completionDocsOf(item: CompletionItem, highlightLanguage: string
400
453
  const source = hasSource ? lines[0]!.trim() : '';
401
454
  const code = (hasSource ? lines.slice(1) : lines).join('\n').trim();
402
455
  const parsed = splitSignatures(documentationText(item.documentation));
456
+
403
457
  const text: HoverText = {
404
458
  signatures: code === '' ? parsed.signatures : [{ language: highlightLanguage, code }, ...parsed.signatures],
405
459
  markdown: parsed.markdown
406
460
  };
461
+
407
462
  return source === '' && isEmptyHover(text) ? null : { source, text };
408
463
  }
409
464
 
@@ -475,6 +530,7 @@ export interface RowWindow {
475
530
  */
476
531
  export function rowWindow(scrollTop: number, active: number, total: number, rowHeight: number, visible: number, margin: number, padding: number): RowWindow {
477
532
  const shown = Math.floor(Math.max(0, scrollTop - padding) / rowHeight);
533
+
478
534
  return {
479
535
  first: Math.max(0, Math.min(shown, active) - margin),
480
536
  last: Math.min(total, Math.max(shown + visible + 1, active + 1) + margin)