@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
@@ -38,6 +38,7 @@ export type ProblemSource = (listener: (report: ProblemReport) => void) => () =>
38
38
  export function serviceProblems(service: LanguageService, pathOf: (uri: string) => string = (uri) => uri): ProblemSource {
39
39
  return (listener) => {
40
40
  const subscription = service.onDiagnostics((report) => listener({ path: pathOf(report.uri), server: report.source, diagnostics: report.diagnostics }));
41
+
41
42
  return () => subscription.dispose();
42
43
  };
43
44
  }
@@ -51,8 +52,10 @@ export class ProjectProblems {
51
52
  private readonly files = new Map<string, ProblemFile>();
52
53
  private readonly listeners = new Set<() => void>();
53
54
  private readonly stop: () => void;
55
+
54
56
  /* The paths of `files` in order; null once a file came or went. */
55
57
  private order: string[] | null = [];
58
+
56
59
  /* Made on first read after a report, so a project nobody lists does no sorting. */
57
60
  private snapshot: readonly ProblemFile[] | null = [];
58
61
 
@@ -65,15 +68,18 @@ export class ProjectProblems {
65
68
  report(path: string, server: string, diagnostics: readonly Diagnostic[]): void {
66
69
  const kept = diagnostics.filter((diagnostic) => severityOf(diagnostic) !== 'hint');
67
70
  const servers = this.reports.get(path) ?? new Map<string, readonly Diagnostic[]>();
71
+
68
72
  if (kept.length === 0 && !servers.has(server)) {
69
73
  // A clean file reported clean again, as servers do after every edit.
70
74
  return;
71
75
  }
76
+
72
77
  if (kept.length === 0) {
73
78
  servers.delete(server);
74
79
  } else {
75
80
  servers.set(server, kept);
76
81
  }
82
+
77
83
  if (servers.size === 0) {
78
84
  this.reports.delete(path);
79
85
  this.files.delete(path);
@@ -82,11 +88,14 @@ export class ProjectProblems {
82
88
  if (!this.files.has(path)) {
83
89
  this.order = null;
84
90
  }
91
+
85
92
  this.reports.set(path, servers);
86
93
  const rows = [...servers].flatMap(([name, list]) => list.map((diagnostic): ProblemRow => ({ diagnostic, server: name }))).sort(compareRows);
87
94
  this.files.set(path, { path, rows });
88
95
  }
96
+
89
97
  this.snapshot = null;
98
+
90
99
  for (const listener of [...this.listeners]) {
91
100
  listener();
92
101
  }
@@ -98,11 +107,13 @@ export class ProjectProblems {
98
107
  this.order ??= [...this.files.keys()].sort((left, right) => left.localeCompare(right));
99
108
  this.snapshot = this.order.map((path) => this.files.get(path)!);
100
109
  }
110
+
101
111
  return this.snapshot;
102
112
  };
103
113
 
104
114
  subscribe = (listener: () => void): (() => void) => {
105
115
  this.listeners.add(listener);
116
+
106
117
  return () => {
107
118
  this.listeners.delete(listener);
108
119
  };
@@ -10,6 +10,7 @@ export interface LineSpan {
10
10
 
11
11
  export function lineSpanOf(range: EditorRange): LineSpan {
12
12
  const endsOnBreak = range.end.character === 0 && range.end.line > range.start.line;
13
+
13
14
  return { startLine: range.start.line + 1, endLine: endsOnBreak ? range.end.line : range.end.line + 1 };
14
15
  }
15
16
 
@@ -22,7 +23,9 @@ export function inlineRangeOf(selection: EditorRange, lineLength: (line: number)
22
23
  if (!isEmptyRange(selection)) {
23
24
  return selection;
24
25
  }
26
+
25
27
  const { line } = selection.start;
28
+
26
29
  return { start: { line, character: 0 }, end: { line, character: lineLength(line) } };
27
30
  }
28
31
 
@@ -58,19 +61,24 @@ const OPENING = /^( {0,3})(`{3,}|~{3,})(.*)$/;
58
61
 
59
62
  function openingOf(line: string): FenceStart | null {
60
63
  const match = OPENING.exec(line);
64
+
61
65
  if (match === null) {
62
66
  return null;
63
67
  }
68
+
64
69
  const [, indent = '', marks = '', info = ''] = match;
70
+
65
71
  // The info string of a backtick fence holds no backtick, which is what keeps a line of inline code from opening one.
66
72
  if (marks.startsWith('`') && info.includes('`')) {
67
73
  return null;
68
74
  }
75
+
69
76
  return { char: marks[0]!, length: marks.length, indent: indent.length, info: info.trim() };
70
77
  }
71
78
 
72
79
  function closes(line: string, start: FenceStart): boolean {
73
80
  const match = /^ {0,3}(`+|~+)\s*$/.exec(line);
81
+
74
82
  return match !== null && match[1]![0] === start.char && match[1]!.length >= start.length;
75
83
  }
76
84
 
@@ -97,26 +105,35 @@ export function parseAnswer(text: string): InlineAnswer {
97
105
  const lines = text.replace(/\r\n/g, '\n').split('\n');
98
106
  let open: { start: FenceStart; from: number; body: string[] } | null = null;
99
107
  let found: { from: number; to: number; body: string[] } | null = null;
108
+
100
109
  for (const [index, line] of lines.entries()) {
101
110
  if (open === null) {
102
111
  const start = openingOf(line);
112
+
103
113
  if (start !== null) {
104
114
  open = { start, from: index, body: [] };
105
115
  }
116
+
106
117
  continue;
107
118
  }
119
+
108
120
  if (closes(line, open.start)) {
109
121
  if (isReplacement(open.start.info)) {
110
122
  found = { from: open.from, to: index, body: open.body };
111
123
  }
124
+
112
125
  open = null;
126
+
113
127
  continue;
114
128
  }
129
+
115
130
  open.body.push(line.replace(new RegExp(`^ {0,${open.start.indent}}`), ''));
116
131
  }
132
+
117
133
  if (found === null) {
118
134
  return { replacement: null, rest: text.trim() };
119
135
  }
136
+
120
137
  return {
121
138
  replacement: found.body.join('\n'),
122
139
  rest: [...lines.slice(0, found.from), ...lines.slice(found.to + 1)]
@@ -131,11 +148,13 @@ export function fitReplacement(selected: string, replacement: string): string {
131
148
  const breaks = selected.includes('\r\n') ? '\r\n' : '\n';
132
149
  let fitted = replacement.replace(/\r\n/g, '\n');
133
150
  const endsOnBreak = selected.endsWith('\n');
151
+
134
152
  if (fitted !== '' && endsOnBreak && !fitted.endsWith('\n')) {
135
153
  fitted += '\n';
136
154
  } else if (!endsOnBreak && fitted.endsWith('\n')) {
137
155
  fitted = fitted.slice(0, -1);
138
156
  }
157
+
139
158
  return breaks === '\n' ? fitted : fitted.replace(/\n/g, breaks);
140
159
  }
141
160
 
@@ -151,6 +170,7 @@ export interface DiffSegment {
151
170
 
152
171
  function linesOf(text: string): string[] {
153
172
  const lines = text.replace(/\r\n/g, '\n').split('\n');
173
+
154
174
  return lines.length > 1 && lines.at(-1) === '' ? lines.slice(0, -1) : lines;
155
175
  }
156
176
 
@@ -164,13 +184,16 @@ export function diffSegments(selected: string, proposal: string, startLine: numb
164
184
  const other = linesOf(proposal);
165
185
  const segments: DiffSegment[] = [];
166
186
  let cursor = 0;
187
+
167
188
  const same = (until: number): void => {
168
189
  if (until > cursor) {
169
190
  segments.push({ kind: 'same', text: other.slice(cursor, until).join('\n'), firstLine: startLine + cursor });
170
191
  }
171
192
  };
193
+
172
194
  for (const change of diffLines(base, other)) {
173
195
  same(change.otherStart);
196
+
174
197
  if (change.otherEnd > change.otherStart) {
175
198
  segments.push({
176
199
  kind: 'added',
@@ -181,9 +204,12 @@ export function diffSegments(selected: string, proposal: string, startLine: numb
181
204
  } else {
182
205
  segments.push({ kind: 'removed', text: base.slice(change.baseStart, change.baseEnd).join('\n') });
183
206
  }
207
+
184
208
  cursor = change.otherEnd;
185
209
  }
210
+
186
211
  same(other.length);
212
+
187
213
  return segments;
188
214
  }
189
215
 
@@ -192,22 +218,29 @@ export function emphasisOf(segment: DiffSegment): Array<Array<[number, number]>>
192
218
  if (segment.kind !== 'added' || segment.replaces === undefined) {
193
219
  return undefined;
194
220
  }
221
+
195
222
  const ranges = replacedWords(linesOf(segment.text), linesOf(segment.replaces));
223
+
196
224
  return ranges.some((line) => line.length > 0) ? ranges : undefined;
197
225
  }
198
226
 
199
227
  /* The offset of a position in a text whose lines end in `\n` or `\r\n`; null when the text is not that long. */
200
228
  export function offsetOf(text: string, position: EditorPosition): number | null {
201
229
  let offset = 0;
230
+
202
231
  for (let line = 0; line < position.line; line++) {
203
232
  const end = text.indexOf('\n', offset);
233
+
204
234
  if (end === -1) {
205
235
  return null;
206
236
  }
237
+
207
238
  offset = end + 1;
208
239
  }
240
+
209
241
  const lineEnd = text.indexOf('\n', offset);
210
242
  const length = (lineEnd === -1 ? text.length : lineEnd) - offset;
243
+
211
244
  return position.character > length ? null : offset + position.character;
212
245
  }
213
246
 
@@ -215,6 +248,7 @@ export function offsetOf(text: string, position: EditorPosition): number | null
215
248
  export function textInRange(text: string, range: EditorRange): string | null {
216
249
  const from = offsetOf(text, range.start);
217
250
  const to = offsetOf(text, range.end);
251
+
218
252
  return from === null || to === null || to < from ? null : text.slice(from, to);
219
253
  }
220
254
 
@@ -222,6 +256,7 @@ export function textInRange(text: string, range: EditorRange): string | null {
222
256
  export function replaceRange(text: string, range: EditorRange, replacement: string): string | null {
223
257
  const from = offsetOf(text, range.start);
224
258
  const to = offsetOf(text, range.end);
259
+
225
260
  return from === null || to === null || to < from ? null : `${text.slice(0, from)}${replacement}${text.slice(to)}`;
226
261
  }
227
262
 
@@ -233,18 +268,23 @@ export function locateSelection(text: string, range: EditorRange, selected: stri
233
268
  if (textInRange(text, range) === selected) {
234
269
  return range;
235
270
  }
271
+
236
272
  if (selected === '') {
237
273
  return null;
238
274
  }
275
+
239
276
  const first = text.indexOf(selected);
277
+
240
278
  if (first === -1 || text.indexOf(selected, first + 1) !== -1) {
241
279
  return null;
242
280
  }
281
+
243
282
  return { start: positionAt(text, first), end: positionAt(text, first + selected.length) };
244
283
  }
245
284
 
246
285
  function positionAt(text: string, offset: number): EditorPosition {
247
286
  const before = text.slice(0, offset);
248
287
  const line = before.split('\n').length - 1;
288
+
249
289
  return { line, character: offset - (before.lastIndexOf('\n') + 1) };
250
290
  }
@@ -20,6 +20,7 @@ export class RecentChoices {
20
20
  const key = RecentChoices.keyOf(item);
21
21
  this.order.delete(key);
22
22
  this.order.set(key, ++this.tick);
23
+
23
24
  if (this.order.size > LIMIT) {
24
25
  this.order.delete(this.order.keys().next().value!);
25
26
  }
@@ -35,14 +36,11 @@ const stores = new Map<string, RecentChoices>();
35
36
 
36
37
  export function recentChoicesOf(languageId: string): RecentChoices {
37
38
  let store = stores.get(languageId);
39
+
38
40
  if (store === undefined) {
39
41
  store = new RecentChoices();
40
42
  stores.set(languageId, store);
41
43
  }
42
- return store;
43
- }
44
44
 
45
- /* For a test that must not hear the choices an earlier one made. */
46
- export function forgetRecentChoices(): void {
47
- stores.clear();
45
+ return store;
48
46
  }
package/src/refresher.ts CHANGED
@@ -29,10 +29,12 @@ export class Refresher {
29
29
  if (this.disposed) {
30
30
  return;
31
31
  }
32
+
32
33
  this.timers.clear(this.timer);
33
34
  this.controller?.abort();
34
35
  const controller = new AbortController();
35
36
  this.controller = controller;
37
+
36
38
  this.run(controller.signal).catch((error: unknown) => {
37
39
  // A server that cannot answer yet is asked again when it says it can; a stale text is asked again by its own change.
38
40
  if (!(error instanceof StaleResultError) && !controller.signal.aborted && !isQuiet(error)) {
@@ -64,5 +66,6 @@ const QUIET_CODES: ReadonlySet<number> = new Set([
64
66
  /* A server that is not up, does not offer the feature or was asked to stop is not worth a line in the console. */
65
67
  function isQuiet(error: unknown): boolean {
66
68
  const code = (error as { code?: number } | null)?.code;
69
+
67
70
  return code !== undefined && QUIET_CODES.has(code);
68
71
  }
@@ -8,12 +8,15 @@ import { textEntriesOf } from './workspace-edit.ts';
8
8
  export function wordRangeAt(lineText: string, position: EditorPosition): EditorRange | null {
9
9
  let start = Math.min(position.character, lineText.length);
10
10
  let end = start;
11
+
11
12
  while (start > 0 && isIdentifierCharacter(lineText[start - 1]!)) {
12
13
  start--;
13
14
  }
15
+
14
16
  while (end < lineText.length && isIdentifierCharacter(lineText[end]!)) {
15
17
  end++;
16
18
  }
19
+
17
20
  return start === end ? null : { start: { line: position.line, character: start }, end: { line: position.line, character: end } };
18
21
  }
19
22
 
@@ -35,13 +38,17 @@ export function renameTargetOf(
35
38
  if (result === null) {
36
39
  return null;
37
40
  }
41
+
38
42
  if ('placeholder' in result) {
39
43
  return { range: result.range, placeholder: result.placeholder };
40
44
  }
45
+
41
46
  if ('start' in result) {
42
47
  return { range: result, placeholder: textOf(result) };
43
48
  }
49
+
44
50
  const word = wordAtCaret();
51
+
45
52
  return word === null ? null : { range: word, placeholder: textOf(word) };
46
53
  }
47
54
 
@@ -99,22 +106,30 @@ export function renameKindOf(result: DocumentSymbolResult, name: string, places:
99
106
  if (result === null || result.length === 0) {
100
107
  return undefined;
101
108
  }
109
+
102
110
  const named = (range: EditorRange) => places.some((place) => rangeHolds(range, place.start));
111
+
103
112
  if ('range' in result[0]!) {
104
113
  let found: DocumentSymbol | undefined;
114
+
105
115
  const walk = (symbols: readonly DocumentSymbol[]): void => {
106
116
  for (const symbol of symbols) {
107
117
  if (named(symbol.selectionRange)) {
108
118
  found = symbol;
109
119
  }
120
+
110
121
  walk(symbol.children ?? []);
111
122
  }
112
123
  };
124
+
113
125
  walk(result as DocumentSymbol[]);
126
+
114
127
  return found === undefined ? undefined : SYMBOL_KINDS[found.kind];
115
128
  }
129
+
116
130
  // A flat list only gives the whole span of a symbol, so the name has to match as well.
117
131
  const found = result.find((symbol) => 'location' in symbol && symbol.name === name && named(symbol.location.range));
132
+
118
133
  return found === undefined ? undefined : SYMBOL_KINDS[found.kind];
119
134
  }
120
135
 
@@ -133,8 +148,10 @@ export interface RenameFile {
133
148
 
134
149
  function lineStarts(text: string): number[] {
135
150
  const starts = [0];
151
+
136
152
  for (let index = 0; index < text.length; index++) {
137
153
  const code = text.charCodeAt(index);
154
+
138
155
  if (code === 13 && text.charCodeAt(index + 1) === 10) {
139
156
  index++;
140
157
  starts.push(index + 1);
@@ -142,34 +159,43 @@ function lineStarts(text: string): number[] {
142
159
  starts.push(index + 1);
143
160
  }
144
161
  }
162
+
145
163
  return starts;
146
164
  }
147
165
 
148
166
  /* The lines of a file an edit touches, each once, as they read before and after. An edit across lines is shown from its first line. */
149
167
  export function renameRowsOf(text: string, edits: readonly TextEdit[]): RenameRow[] {
150
168
  const starts = lineStarts(text);
169
+
151
170
  const lineAt = (line: number): { from: number; to: number } => {
152
171
  const from = starts[line] ?? text.length;
153
172
  let to = (starts[line + 1] ?? text.length + 1) - 1;
173
+
154
174
  while (to > from && (text[to - 1] === '\n' || text[to - 1] === '\r')) {
155
175
  to--;
156
176
  }
177
+
157
178
  return { from, to: Math.min(to, text.length) };
158
179
  };
180
+
159
181
  const byLine = new Map<number, TextEdit[]>();
182
+
160
183
  for (const edit of edits) {
161
184
  byLine.set(edit.range.start.line, [...(byLine.get(edit.range.start.line) ?? []), edit]);
162
185
  }
186
+
163
187
  return [...byLine]
164
188
  .sort((left, right) => left[0] - right[0])
165
189
  .map(([line, own]) => {
166
190
  const { from, to } = lineAt(line);
167
191
  const before = text.slice(from, to);
168
192
  let after = before;
193
+
169
194
  for (const edit of [...own].sort((left, right) => right.range.start.character - left.range.start.character)) {
170
195
  const end = edit.range.end.line === line ? edit.range.end.character : before.length;
171
196
  after = after.slice(0, edit.range.start.character) + edit.newText + after.slice(end);
172
197
  }
198
+
173
199
  return { line: line + 1, before, after };
174
200
  });
175
201
  }
@@ -177,9 +203,11 @@ export function renameRowsOf(text: string, edits: readonly TextEdit[]): RenameRo
177
203
  /* The lines of the files an edit edits; the files it makes, moves or deletes are listed by `resourceOperationsOf`. */
178
204
  export async function renamePreviewOf(edit: WorkspaceEdit, textOf: (uri: string) => Promise<string | null>): Promise<RenameFile[]> {
179
205
  const files: RenameFile[] = [];
206
+
180
207
  for (const [uri, list] of textEntriesOf(edit)) {
181
208
  const text = await textOf(uri);
182
209
  files.push({ uri, rows: text === null ? [] : renameRowsOf(text, list.flat()) });
183
210
  }
211
+
184
212
  return files;
185
213
  }
package/src/rename.ts CHANGED
@@ -11,8 +11,10 @@ import { messageOf } from './error-message.ts';
11
11
 
12
12
  const METHOD = 'textDocument/rename';
13
13
  const TOAST_ID = 'language-rename';
14
+
14
15
  /* How much is read to tell a name suggester how the symbol is used: its first places, other files and lines around it. */
15
16
  const SAMPLE_LOCATIONS = 8;
17
+
16
18
  const SAMPLE_OTHER_FILES = 3;
17
19
  const CONTEXT_LINES = 3;
18
20
 
@@ -37,20 +39,25 @@ export class RenameFeature {
37
39
  constructor(language: EditorLanguage) {
38
40
  this.language = language;
39
41
  const { editor } = language;
42
+
40
43
  const offs = [
41
44
  language.onKey((event) => {
42
45
  if (!isShortcut(this.language.shortcuts.rename, event)) {
43
46
  return false;
44
47
  }
48
+
45
49
  void this.start();
50
+
46
51
  return true;
47
52
  }, KEY_PRIORITY.rename),
48
53
  editor.onTextChange(() => this.cancel())
49
54
  ];
55
+
50
56
  language.onDispose(() => {
51
57
  for (const off of offs) {
52
58
  off();
53
59
  }
60
+
54
61
  this.cancel();
55
62
  });
56
63
  }
@@ -61,31 +68,40 @@ export class RenameFeature {
61
68
 
62
69
  async start(): Promise<void> {
63
70
  const { editor, project, uri } = this.language;
71
+
64
72
  if (!project.service.supports(METHOD, uri)) {
65
73
  this.tell('error', this.say('unavailable'));
74
+
66
75
  return;
67
76
  }
77
+
68
78
  this.cancel();
69
79
  const caret = editor.getCaret();
70
80
  const prepare = project.service.providerOptions(METHOD, uri)?.prepareProvider === true;
71
81
  let answer: PrepareRenameResult;
82
+
72
83
  try {
73
84
  answer = prepare ? await project.service.prepareRename(uri, caret) : { defaultBehavior: true };
74
85
  } catch (error) {
75
86
  if (!(error instanceof StaleResultError)) {
76
87
  this.tell('error', this.say('nothing'));
77
88
  }
89
+
78
90
  return;
79
91
  }
92
+
80
93
  const target = renameTargetOf(
81
94
  answer,
82
95
  (range) => editor.textInRange(range),
83
96
  () => wordRangeAt(lineTextOf(editor, caret.line), caret)
84
97
  );
98
+
85
99
  if (target === null) {
86
100
  this.tell('error', this.say('nothing'));
101
+
87
102
  return;
88
103
  }
104
+
89
105
  const session: Session = { target, original: editor.textInRange(target.range), edit: null, names: new AbortController() };
90
106
  this.session = session;
91
107
  this.publish({ phase: 'input', busy: false, name: target.placeholder, files: [], operations: [], occurrences: null, suggestions: [] });
@@ -96,33 +112,47 @@ export class RenameFeature {
96
112
  async submit(name: string, preview: boolean): Promise<void> {
97
113
  const session = this.session;
98
114
  const next = name.trim();
115
+
99
116
  if (session === null) {
100
117
  return;
101
118
  }
119
+
102
120
  if (next === '' || next === session.original) {
103
121
  this.finish();
122
+
104
123
  return;
105
124
  }
125
+
106
126
  this.publish({ phase: 'input', busy: true, name: next, error: null });
107
127
  const { project, uri } = this.language;
128
+
108
129
  try {
109
130
  const edit = await project.service.rename(uri, session.target.range.start, next);
131
+
110
132
  if (this.session !== session) {
111
133
  return;
112
134
  }
135
+
113
136
  if (edit === null) {
114
137
  this.fail(this.say('refused'));
138
+
115
139
  return;
116
140
  }
141
+
117
142
  session.edit = edit;
143
+
118
144
  if (!preview) {
119
145
  await this.apply(session);
146
+
120
147
  return;
121
148
  }
149
+
122
150
  const files = await renamePreviewOf(edit, (target) => project.readText(target));
151
+
123
152
  if (this.session !== session) {
124
153
  return;
125
154
  }
155
+
126
156
  this.publish({ phase: 'preview', busy: false, name: next, files, operations: resourceOperationsOf(edit) });
127
157
  } catch (error) {
128
158
  if (this.session === session) {
@@ -134,6 +164,7 @@ export class RenameFeature {
134
164
  /* Applies the edit the preview showed. */
135
165
  async confirm(): Promise<void> {
136
166
  const session = this.session;
167
+
137
168
  if (session?.edit) {
138
169
  await this.apply(session);
139
170
  }
@@ -143,6 +174,7 @@ export class RenameFeature {
143
174
  if (this.session === null) {
144
175
  return;
145
176
  }
177
+
146
178
  this.finish();
147
179
  }
148
180
 
@@ -155,9 +187,11 @@ export class RenameFeature {
155
187
 
156
188
  private async apply(session: Session): Promise<void> {
157
189
  const result = await this.language.project.applyWorkspaceEdit(session.edit!);
190
+
158
191
  if (this.session !== session) {
159
192
  return;
160
193
  }
194
+
161
195
  if (result.applied) {
162
196
  this.finish();
163
197
  } else {
@@ -185,17 +219,22 @@ export class RenameFeature {
185
219
  /* Lights up the places the name has in this file, and counts them all; what the servers listed, or null when they did not. */
186
220
  private async light(session: Session): Promise<readonly Location[] | null> {
187
221
  const { editor, project, uri } = this.language;
222
+
188
223
  if (!project.service.supports('textDocument/references', uri)) {
189
224
  return null;
190
225
  }
226
+
191
227
  try {
192
228
  const locations = await project.service.references(uri, session.target.range.start, true);
229
+
193
230
  if (this.session !== session || locations === null) {
194
231
  return null;
195
232
  }
233
+
196
234
  const found = occurrencesOf(locations, uri);
197
235
  editor.setHighlights(found.inFile.map((range: EditorRange) => ({ range, kind: 'write' as const })));
198
236
  this.publish({ occurrences: { count: found.count, files: found.files } });
237
+
199
238
  return locations;
200
239
  } catch {
201
240
  // The count is a courtesy; renaming does not wait for it.
@@ -209,19 +248,25 @@ export class RenameFeature {
209
248
  */
210
249
  private async suggest(session: Session, locations: readonly Location[] | null): Promise<void> {
211
250
  const suggestNames = this.language.project.host.suggestNames;
251
+
212
252
  if (suggestNames === undefined || this.session !== session) {
213
253
  return;
214
254
  }
255
+
215
256
  const { editor, languageId, uri } = this.language;
257
+
216
258
  try {
217
259
  const line = session.target.range.start.line;
260
+
218
261
  const context = editor.textInRange({
219
262
  start: { line: Math.max(0, line - CONTEXT_LINES), character: 0 },
220
263
  end: { line: line + CONTEXT_LINES, character: Number.MAX_SAFE_INTEGER }
221
264
  });
265
+
222
266
  const [uses, kind] = await Promise.all([this.linesOf(locations ?? []), this.kindOf(session, locations ?? [])]);
223
267
  const request = { uri, languageId, name: session.original, ...(kind === undefined ? {} : { kind }), context, uses };
224
268
  const names = await suggestNames(request, session.names.signal);
269
+
225
270
  if (this.session === session && names.length > 0) {
226
271
  this.publish({ suggestions: names });
227
272
  }
@@ -233,11 +278,14 @@ export class RenameFeature {
233
278
  /* So a suggester knows a function from a variable; a rename on a use finds the kind through the declaration's place in this file. */
234
279
  private async kindOf(session: Session, locations: readonly Location[]): Promise<string | undefined> {
235
280
  const { project, uri } = this.language;
281
+
236
282
  if (!project.service.supports('textDocument/documentSymbol', uri)) {
237
283
  return undefined;
238
284
  }
285
+
239
286
  try {
240
287
  const symbols = await project.service.documentSymbols(uri, { signal: session.names.signal, parallel: true });
288
+
241
289
  return renameKindOf(symbols, session.original, [session.target.range, ...occurrencesOf(locations, uri).inFile]);
242
290
  } catch {
243
291
  return undefined;
@@ -250,28 +298,36 @@ export class RenameFeature {
250
298
  const texts = new Map<string, string[] | null>();
251
299
  const lines: string[] = [];
252
300
  let others = 0;
301
+
253
302
  for (const location of locations.slice(0, SAMPLE_LOCATIONS)) {
254
303
  if (!texts.has(location.uri)) {
255
304
  if (location.uri !== this.language.uri && others++ >= SAMPLE_OTHER_FILES) {
256
305
  continue;
257
306
  }
307
+
258
308
  texts.set(location.uri, (await project.readText(location.uri))?.split('\n') ?? null);
259
309
  }
310
+
260
311
  const text = texts.get(location.uri)?.[location.range.start.line];
312
+
261
313
  if (text !== undefined && text.trim() !== '' && !lines.includes(text)) {
262
314
  lines.push(text);
263
315
  }
264
316
  }
317
+
265
318
  return lines;
266
319
  }
267
320
 
268
321
  private publish(patch: Partial<RenameView>): void {
269
322
  const session = this.session;
323
+
270
324
  if (session === null) {
271
325
  return;
272
326
  }
327
+
273
328
  const { popups } = this.language;
274
329
  const current = popups.getState().rename;
330
+
275
331
  popups.setState({
276
332
  rename: {
277
333
  phase: 'input',