@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.
- package/dist/CallHierarchyPanel.js +30 -55
- package/dist/CodeAuthorsCard.js +0 -1
- package/dist/CompletionPopup.js +150 -181
- package/dist/DiffView.d.ts +1 -1
- package/dist/DiffView.js +2 -29
- package/dist/PickPopup.js +54 -80
- package/dist/SplitDiffView.js +27 -57
- package/dist/SymbolPicker.js +2 -8
- package/dist/code-actions-model.js +2 -9
- package/dist/code-actions.js +10 -11
- package/dist/code-vision.js +2 -1
- package/dist/completion-model.js +2 -2
- package/dist/completion.js +13 -34
- package/dist/definition-link.d.ts +1 -0
- package/dist/definition-link.js +22 -18
- package/dist/diagnostics-model.d.ts +1 -0
- package/dist/diagnostics-model.js +3 -1
- package/dist/diagnostics.js +1 -2
- package/dist/diff-syntax.d.ts +1 -0
- package/dist/diff-syntax.js +31 -0
- package/dist/doc-comment-model.d.ts +1 -0
- package/dist/doc-comment-model.js +1 -1
- package/dist/folding.js +2 -3
- package/dist/format-on-type.js +6 -10
- package/dist/hover-content.d.ts +0 -2
- package/dist/hover-content.js +1 -9
- package/dist/inlay-hints.d.ts +0 -1
- package/dist/inlay-hints.js +11 -41
- package/dist/navigation.js +4 -14
- package/dist/peek.js +2 -12
- package/dist/popup-focus.d.ts +3 -1
- package/dist/popup-focus.js +33 -0
- package/dist/recent-choices.d.ts +0 -1
- package/dist/recent-choices.js +0 -4
- package/dist/rendered-docs.d.ts +1 -0
- package/dist/rendered-docs.js +15 -28
- package/dist/snippet-field-transforms.d.ts +5 -0
- package/dist/snippet-field-transforms.js +27 -16
- package/dist/snippet-session.js +8 -20
- package/dist/symbol-links.js +3 -10
- package/dist/symbol-picker-model.d.ts +1 -0
- package/dist/symbol-picker-model.js +7 -0
- package/dist/unique.d.ts +10 -0
- package/dist/unique.js +15 -0
- package/package.json +9 -9
- package/src/AnchoredPopup.tsx +2 -0
- package/src/BaselineStatus.tsx +1 -0
- package/src/CallHierarchyPanel.tsx +16 -6
- package/src/ChangeReview.tsx +7 -0
- package/src/CodeAuthorsCard.tsx +2 -5
- package/src/CompletionPopup.tsx +21 -15
- package/src/DiffView.tsx +53 -30
- package/src/EditorContextMenu.tsx +7 -0
- package/src/EditorView.tsx +28 -0
- package/src/FindReplace.tsx +8 -0
- package/src/HoverCard.tsx +8 -0
- package/src/HoverSections.tsx +11 -0
- package/src/LanguagePopups.tsx +1 -0
- package/src/PanelResizeHandle.tsx +8 -0
- package/src/PathText.tsx +1 -0
- package/src/PeekPanel.tsx +13 -0
- package/src/PickPopup.tsx +7 -6
- package/src/RenameCard.tsx +13 -0
- package/src/RenderedDocs.tsx +11 -0
- package/src/SplitDiffView.tsx +91 -56
- package/src/SymbolPicker.tsx +10 -18
- package/src/call-hierarchy-model.ts +10 -0
- package/src/call-hierarchy.ts +82 -0
- package/src/code-actions-model.ts +21 -9
- package/src/code-actions.ts +83 -7
- package/src/code-authors.ts +20 -0
- package/src/code-vision-model.ts +37 -0
- package/src/code-vision.ts +47 -3
- package/src/completion-call.ts +19 -0
- package/src/completion-model.ts +58 -2
- package/src/completion.ts +156 -30
- package/src/conflict-model.ts +4 -0
- package/src/context-menu-rows.ts +1 -0
- package/src/context-menu.ts +7 -0
- package/src/definition-link.ts +42 -18
- package/src/diagnostics-model.ts +19 -1
- package/src/diagnostics.ts +31 -2
- package/src/diff-expansion.ts +9 -0
- package/src/diff-overlays.tsx +8 -0
- package/src/diff-syntax.ts +54 -0
- package/src/diff-wordmarks.ts +8 -0
- package/src/doc-comment-model.ts +51 -1
- package/src/editor-language.ts +11 -0
- package/src/find-query.ts +7 -0
- package/src/folding.ts +10 -3
- package/src/format-on-type.ts +23 -10
- package/src/highlight-layers.ts +3 -0
- package/src/highlights.ts +18 -0
- package/src/history.ts +11 -0
- package/src/holder.ts +3 -0
- package/src/hover-content.ts +34 -17
- package/src/hover.ts +57 -0
- package/src/index.ts +4 -0
- package/src/inlay-hints.ts +86 -47
- package/src/key-priority.ts +2 -0
- package/src/line-edits.ts +8 -0
- package/src/models.ts +6 -0
- package/src/navigation-history.ts +5 -0
- package/src/navigation.ts +28 -13
- package/src/panel-size.ts +7 -0
- package/src/patch-decorations.ts +39 -0
- package/src/patch-layout.ts +47 -0
- package/src/peek-model.ts +21 -0
- package/src/peek.ts +49 -13
- package/src/pick.ts +35 -0
- package/src/popup-focus.ts +11 -1
- package/src/popup-placement.ts +5 -0
- package/src/popups.ts +2 -6
- package/src/project-language.ts +33 -0
- package/src/project-problems.ts +11 -0
- package/src/proposal-model.ts +40 -0
- package/src/recent-choices.ts +3 -5
- package/src/refresher.ts +3 -0
- package/src/rename-model.ts +28 -0
- package/src/rename.ts +56 -0
- package/src/rendered-docs.ts +89 -19
- package/src/review-group.ts +4 -0
- package/src/row-host.ts +12 -0
- package/src/selection-ranges.ts +6 -0
- package/src/semantic-delta.ts +7 -0
- package/src/semantic-model.ts +11 -0
- package/src/semantic-tokens.ts +37 -0
- package/src/shortcut-keys.ts +3 -0
- package/src/signature-model.ts +7 -0
- package/src/signature.ts +27 -0
- package/src/snippet-field-transforms.ts +48 -13
- package/src/snippet-indent.ts +10 -0
- package/src/snippet-range-order.ts +5 -0
- package/src/snippet-session.ts +173 -31
- package/src/snippet-transform-client.ts +14 -0
- package/src/snippet-transform-format.ts +98 -0
- package/src/snippet-transform-protocol.ts +5 -0
- package/src/snippet-transform-queue.ts +53 -0
- package/src/snippet-transform-worker.ts +14 -0
- package/src/snippet-variables.ts +9 -0
- package/src/snippet.ts +107 -0
- package/src/split-alignment.ts +10 -0
- package/src/split-scroll.ts +8 -0
- package/src/symbol-links.ts +9 -12
- package/src/symbol-picker-model.ts +25 -0
- package/src/symbol-picker.ts +9 -0
- package/src/symbols.ts +21 -0
- package/src/testing.ts +7 -0
- package/src/timers.ts +5 -0
- package/src/unique.ts +23 -0
- package/src/use-problem-counts.ts +4 -0
- package/src/view-state.ts +9 -0
- package/src/word-diff.ts +5 -0
- package/src/workspace-edit.ts +84 -0
package/src/project-problems.ts
CHANGED
|
@@ -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
|
};
|
package/src/proposal-model.ts
CHANGED
|
@@ -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
|
}
|
package/src/recent-choices.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/rename-model.ts
CHANGED
|
@@ -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',
|