Skip to content

Commit 4c6133f

Browse files
committed
feat: rewrite lsp rename
1 parent 8ff20f0 commit 4c6133f

File tree

4 files changed

+284
-5
lines changed

4 files changed

+284
-5
lines changed

src/cm/commandRegistry.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,15 @@ import {
7272
jumpToTypeDefinition as lspJumpToTypeDefinition,
7373
nextSignature as lspNextSignature,
7474
prevSignature as lspPrevSignature,
75-
renameSymbol as lspRenameSymbol,
7675
showSignatureHelp as lspShowSignatureHelp,
7776
} from "@codemirror/lsp-client";
7877
import { Compartment, EditorSelection } from "@codemirror/state";
7978
import { keymap } from "@codemirror/view";
80-
import { clearDiagnosticsEffect, clientManager } from "cm/lsp";
79+
import {
80+
renameSymbol as acodeRenameSymbol,
81+
clearDiagnosticsEffect,
82+
clientManager,
83+
} from "cm/lsp";
8184
import toast from "components/toast";
8285
import prompt from "dialogs/prompt";
8386
import actions from "handlers/quickTools";
@@ -882,7 +885,7 @@ function registerLspCommands() {
882885
description: "Rename symbol (Language Server)",
883886
readOnly: false,
884887
requiresView: true,
885-
run: runLspCommand(lspRenameSymbol),
888+
run: runLspCommand(acodeRenameSymbol),
886889
});
887890
addCommand({
888891
name: "showSignatureHelp",

src/cm/lsp/clientManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
jumpToDefinitionKeymap,
88
LSPClient,
99
LSPPlugin,
10-
renameKeymap,
1110
serverCompletion,
1211
serverDiagnostics,
1312
signatureHelp,
@@ -20,6 +19,7 @@ import Uri from "utils/Uri";
2019
import { clearDiagnosticsEffect } from "./diagnostics";
2120
import { documentHighlightsExtension } from "./documentHighlights";
2221
import { inlayHintsExtension } from "./inlayHints";
22+
import { acodeRenameKeymap } from "./rename";
2323
import { ensureServerRunning } from "./serverLauncher";
2424
import serverRegistry from "./serverRegistry";
2525
import { createTransport } from "./transport";
@@ -62,7 +62,7 @@ function safeString(value: unknown): string {
6262

6363
const defaultKeymaps = keymap.of([
6464
...formatKeymap,
65-
...renameKeymap,
65+
...acodeRenameKeymap,
6666
...jumpToDefinitionKeymap,
6767
...findReferencesKeymap,
6868
]);

src/cm/lsp/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export {
2020
inlayHintsEditorExtension,
2121
inlayHintsExtension,
2222
} from "./inlayHints";
23+
export {
24+
acodeRenameExtension,
25+
acodeRenameKeymap,
26+
renameSymbol,
27+
} from "./rename";
2328
export {
2429
ensureServerRunning,
2530
resetManagedServers,

src/cm/lsp/rename.ts

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import { LSPPlugin } from "@codemirror/lsp-client";
2+
import {
3+
type Command,
4+
EditorView,
5+
type KeyBinding,
6+
keymap,
7+
} from "@codemirror/view";
8+
import prompt from "dialogs/prompt";
9+
import type * as lsp from "vscode-languageserver-protocol";
10+
import type AcodeWorkspace from "./workspace";
11+
12+
interface RenameParams {
13+
newName: string;
14+
position: lsp.Position;
15+
textDocument: { uri: string };
16+
}
17+
18+
interface TextDocumentEdit {
19+
range: lsp.Range;
20+
newText: string;
21+
}
22+
23+
interface PrepareRenameResponse {
24+
range?: lsp.Range;
25+
placeholder?: string;
26+
defaultBehavior?: boolean;
27+
}
28+
29+
interface LspChange {
30+
range: lsp.Range;
31+
newText: string;
32+
}
33+
34+
function getRename(plugin: LSPPlugin, pos: number, newName: string) {
35+
return plugin.client.request<RenameParams, lsp.WorkspaceEdit | null>(
36+
"textDocument/rename",
37+
{
38+
newName,
39+
position: plugin.toPosition(pos),
40+
textDocument: { uri: plugin.uri },
41+
},
42+
);
43+
}
44+
45+
function getPrepareRename(plugin: LSPPlugin, pos: number) {
46+
return plugin.client.request<
47+
{ position: lsp.Position; textDocument: { uri: string } },
48+
PrepareRenameResponse | lsp.Range | null
49+
>("textDocument/prepareRename", {
50+
position: plugin.toPosition(pos),
51+
textDocument: { uri: plugin.uri },
52+
});
53+
}
54+
55+
async function performRename(view: EditorView): Promise<boolean> {
56+
const wordRange = view.state.wordAt(view.state.selection.main.head);
57+
const plugin = LSPPlugin.get(view);
58+
59+
if (!plugin) {
60+
return false;
61+
}
62+
63+
const capabilities = plugin.client.serverCapabilities;
64+
const renameProvider = capabilities?.renameProvider;
65+
66+
if (renameProvider === false || renameProvider === undefined) {
67+
return false;
68+
}
69+
70+
if (!wordRange) {
71+
return false;
72+
}
73+
74+
const word = view.state.sliceDoc(wordRange.from, wordRange.to);
75+
let initialValue = word;
76+
let canRename = true;
77+
78+
const supportsPrepare =
79+
typeof renameProvider === "object" &&
80+
renameProvider !== null &&
81+
"prepareProvider" in renameProvider &&
82+
renameProvider.prepareProvider === true;
83+
84+
if (supportsPrepare) {
85+
try {
86+
plugin.client.sync();
87+
const prepareResult = await getPrepareRename(plugin, wordRange.from);
88+
if (prepareResult === null) {
89+
canRename = false;
90+
} else if (typeof prepareResult === "object" && prepareResult !== null) {
91+
if ("placeholder" in prepareResult && prepareResult.placeholder) {
92+
initialValue = prepareResult.placeholder;
93+
} else if (
94+
"defaultBehavior" in prepareResult &&
95+
prepareResult.defaultBehavior
96+
) {
97+
initialValue = word;
98+
} else if ("start" in prepareResult && "end" in prepareResult) {
99+
const from = plugin.fromPosition(prepareResult.start);
100+
const to = plugin.fromPosition(prepareResult.end);
101+
initialValue = view.state.sliceDoc(from, to);
102+
} else if ("range" in prepareResult && prepareResult.range) {
103+
const from = plugin.fromPosition(prepareResult.range.start);
104+
const to = plugin.fromPosition(prepareResult.range.end);
105+
initialValue = view.state.sliceDoc(from, to);
106+
}
107+
}
108+
} catch (error) {
109+
console.warn("[LSP:Rename] prepareRename failed, using word:", error);
110+
}
111+
}
112+
113+
if (!canRename) {
114+
const alert = (await import("dialogs/alert")).default;
115+
alert("Rename", "Cannot rename this symbol.");
116+
return true;
117+
}
118+
119+
const newName = await prompt(
120+
strings["new name"] || "New name",
121+
initialValue,
122+
"text",
123+
{
124+
required: true,
125+
placeholder: strings["enter new name"] || "Enter new name",
126+
},
127+
);
128+
129+
if (newName === null || newName === initialValue) {
130+
return true;
131+
}
132+
133+
try {
134+
await doRename(view, String(newName), wordRange.from);
135+
} catch (error) {
136+
console.error("[LSP:Rename] Rename failed:", error);
137+
const errorMessage =
138+
error instanceof Error ? error.message : "Failed to rename symbol";
139+
const alert = (await import("dialogs/alert")).default;
140+
alert("Rename Error", errorMessage);
141+
}
142+
143+
return true;
144+
}
145+
146+
function lspPositionToOffset(
147+
doc: { line: (n: number) => { from: number } },
148+
pos: lsp.Position,
149+
): number {
150+
const line = doc.line(pos.line + 1);
151+
return line.from + pos.character;
152+
}
153+
154+
async function applyChangesToFile(
155+
workspace: AcodeWorkspace,
156+
uri: string,
157+
lspChanges: LspChange[],
158+
mapping: { mapPosition: (uri: string, pos: lsp.Position) => number },
159+
): Promise<boolean> {
160+
const file = workspace.getFile(uri);
161+
162+
if (file) {
163+
const view = file.getView();
164+
if (view) {
165+
view.dispatch({
166+
changes: lspChanges.map((change) => ({
167+
from: mapping.mapPosition(uri, change.range.start),
168+
to: mapping.mapPosition(uri, change.range.end),
169+
insert: change.newText,
170+
})),
171+
userEvent: "rename",
172+
});
173+
return true;
174+
}
175+
}
176+
177+
const displayedView = await workspace.displayFile(uri);
178+
if (!displayedView?.state?.doc) {
179+
console.warn(`[LSP:Rename] Could not open file: ${uri}`);
180+
return false;
181+
}
182+
183+
const doc = displayedView.state.doc;
184+
displayedView.dispatch({
185+
changes: lspChanges.map((change) => ({
186+
from: lspPositionToOffset(doc, change.range.start),
187+
to: lspPositionToOffset(doc, change.range.end),
188+
insert: change.newText,
189+
})),
190+
userEvent: "rename",
191+
});
192+
193+
return true;
194+
}
195+
196+
async function doRename(
197+
view: EditorView,
198+
newName: string,
199+
position: number,
200+
): Promise<void> {
201+
const plugin = LSPPlugin.get(view);
202+
if (!plugin) return;
203+
204+
plugin.client.sync();
205+
206+
const response = await plugin.client.withMapping((mapping) =>
207+
getRename(plugin, position, newName).then((response) => {
208+
if (!response) return null;
209+
return { response, mapping };
210+
}),
211+
);
212+
213+
if (!response) {
214+
console.info("[LSP:Rename] No changes returned from server");
215+
return;
216+
}
217+
218+
const { response: workspaceEdit, mapping } = response;
219+
const workspace = plugin.client.workspace as AcodeWorkspace;
220+
let filesChanged = 0;
221+
222+
if (workspaceEdit.changes) {
223+
for (const uri in workspaceEdit.changes) {
224+
const lspChanges = workspaceEdit.changes[uri] as TextDocumentEdit[];
225+
if (!lspChanges.length) continue;
226+
227+
const success = await applyChangesToFile(
228+
workspace,
229+
uri,
230+
lspChanges,
231+
mapping,
232+
);
233+
if (success) filesChanged++;
234+
}
235+
}
236+
237+
if (workspaceEdit.documentChanges) {
238+
for (const docChange of workspaceEdit.documentChanges) {
239+
if ("textDocument" in docChange && "edits" in docChange) {
240+
const uri = docChange.textDocument.uri;
241+
const edits = docChange.edits as TextDocumentEdit[];
242+
if (!edits.length) continue;
243+
244+
const success = await applyChangesToFile(
245+
workspace,
246+
uri,
247+
edits,
248+
mapping,
249+
);
250+
if (success) filesChanged++;
251+
}
252+
}
253+
}
254+
255+
console.info(
256+
`[LSP:Rename] Renamed to "${newName}" in ${filesChanged} file(s)`,
257+
);
258+
}
259+
260+
export const renameSymbol: Command = (view) => {
261+
performRename(view).catch((error) => {
262+
console.error("[LSP:Rename] Rename command failed:", error);
263+
});
264+
return true;
265+
};
266+
267+
export const acodeRenameKeymap: readonly KeyBinding[] = [
268+
{ key: "F2", run: renameSymbol, preventDefault: true },
269+
];
270+
271+
export const acodeRenameExtension = () => keymap.of([...acodeRenameKeymap]);

0 commit comments

Comments
 (0)