Skip to content

Commit 5aaedfa

Browse files
committed
feat: Implement "Find All reference"
1 parent daf4e4d commit 5aaedfa

File tree

7 files changed

+1451
-4
lines changed

7 files changed

+1451
-4
lines changed

src/cm/commandRegistry.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ import {
8181
clearDiagnosticsEffect,
8282
clientManager,
8383
} from "cm/lsp";
84+
import {
85+
closeReferencesPanel as acodeCloseReferencesPanel,
86+
findAllReferences as acodeFindAllReferences,
87+
findAllReferencesInTab as acodeFindAllReferencesInTab,
88+
} from "cm/lsp/references";
8489
import toast from "components/toast";
8590
import prompt from "dialogs/prompt";
8691
import actions from "handlers/quickTools";
@@ -938,20 +943,43 @@ function registerLspCommands() {
938943
});
939944
addCommand({
940945
name: "findReferences",
941-
description: "Find references (Language Server)",
946+
description: "Find all references (Language Server)",
942947
readOnly: true,
943948
requiresView: true,
944-
run: runLspCommand(lspFindReferences),
949+
async run(view) {
950+
const resolvedView = resolveView(view);
951+
if (!resolvedView) return false;
952+
const plugin = LSPPlugin.get(resolvedView);
953+
if (!plugin) {
954+
notifyLspUnavailable();
955+
return false;
956+
}
957+
return acodeFindAllReferences(resolvedView);
958+
},
945959
});
946960
addCommand({
947961
name: "closeReferencePanel",
948962
description: "Close references panel",
949963
readOnly: true,
964+
requiresView: false,
965+
run() {
966+
return acodeCloseReferencesPanel();
967+
},
968+
});
969+
addCommand({
970+
name: "findReferencesInTab",
971+
description: "Find all references in new tab (Language Server)",
972+
readOnly: true,
950973
requiresView: true,
951-
run(view) {
974+
async run(view) {
952975
const resolvedView = resolveView(view);
953976
if (!resolvedView) return false;
954-
return lspCloseReferencePanel(resolvedView);
977+
const plugin = LSPPlugin.get(resolvedView);
978+
if (!plugin) {
979+
notifyLspUnavailable();
980+
return false;
981+
}
982+
return acodeFindAllReferencesInTab(resolvedView);
955983
},
956984
});
957985
addCommand({

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+
closeReferencesPanel,
25+
findAllReferences,
26+
findAllReferencesInTab,
27+
} from "./references";
2328
export {
2429
acodeRenameExtension,
2530
acodeRenameKeymap,

src/cm/lsp/references.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import fsOperation from "fileSystem";
2+
import { LSPPlugin } from "@codemirror/lsp-client";
3+
import type { EditorView } from "@codemirror/view";
4+
import {
5+
openReferencesTab,
6+
showReferencesPanel,
7+
} from "components/referencesPanel";
8+
import settings from "lib/settings";
9+
10+
interface Position {
11+
line: number;
12+
character: number;
13+
}
14+
15+
interface Range {
16+
start: Position;
17+
end: Position;
18+
}
19+
20+
interface Location {
21+
uri: string;
22+
range: Range;
23+
}
24+
25+
interface ReferenceWithContext extends Location {
26+
lineText?: string;
27+
}
28+
29+
interface ReferenceParams {
30+
textDocument: { uri: string };
31+
position: Position;
32+
context: { includeDeclaration: boolean };
33+
}
34+
35+
async function fetchLineText(uri: string, line: number): Promise<string> {
36+
try {
37+
interface EditorManagerLike {
38+
getFile?: (uri: string, type: string) => EditorFileLike | null;
39+
}
40+
41+
interface EditorFileLike {
42+
session?: {
43+
doc?: {
44+
line?: (n: number) => { text?: string } | null;
45+
toString?: () => string;
46+
};
47+
};
48+
}
49+
50+
const em = (globalThis as Record<string, unknown>).editorManager as
51+
| EditorManagerLike
52+
| undefined;
53+
54+
const openFile = em?.getFile?.(uri, "uri");
55+
if (openFile?.session?.doc) {
56+
const doc = openFile.session.doc;
57+
if (typeof doc.line === "function") {
58+
const lineObj = doc.line(line + 1);
59+
if (lineObj && typeof lineObj.text === "string") {
60+
return lineObj.text;
61+
}
62+
}
63+
if (typeof doc.toString === "function") {
64+
const content = doc.toString();
65+
const lines = content.split("\n");
66+
if (lines[line] !== undefined) {
67+
return lines[line];
68+
}
69+
}
70+
}
71+
72+
const fs = fsOperation(uri);
73+
if (fs && (await fs.exists())) {
74+
const encoding =
75+
(settings as { value?: { defaultFileEncoding?: string } })?.value
76+
?.defaultFileEncoding || "utf-8";
77+
const content = await fs.readFile(encoding);
78+
if (typeof content === "string") {
79+
const lines = content.split("\n");
80+
if (lines[line] !== undefined) {
81+
return lines[line];
82+
}
83+
}
84+
}
85+
} catch (error) {
86+
console.warn(`Failed to fetch line text for ${uri}:${line}`, error);
87+
}
88+
return "";
89+
}
90+
91+
function getWordAtCursor(view: EditorView): string {
92+
const { state } = view;
93+
const pos = state.selection.main.head;
94+
const word = state.wordAt(pos);
95+
if (word) {
96+
return state.doc.sliceString(word.from, word.to);
97+
}
98+
return "";
99+
}
100+
101+
async function fetchReferences(
102+
view: EditorView,
103+
): Promise<{ symbolName: string; references: ReferenceWithContext[] } | null> {
104+
const plugin = LSPPlugin.get(view);
105+
if (!plugin) {
106+
return null;
107+
}
108+
109+
const client = plugin.client;
110+
const capabilities = client.serverCapabilities;
111+
112+
if (!capabilities?.referencesProvider) {
113+
const toast = (globalThis as Record<string, unknown>).toast as
114+
| ((msg: string) => void)
115+
| undefined;
116+
toast?.("Language server does not support find references");
117+
return null;
118+
}
119+
120+
const { state } = view;
121+
const pos = state.selection.main.head;
122+
const line = state.doc.lineAt(pos);
123+
const lineNumber = line.number - 1;
124+
const character = pos - line.from;
125+
const uri = plugin.uri;
126+
127+
const symbolName = getWordAtCursor(view);
128+
129+
client.sync();
130+
131+
const params: ReferenceParams = {
132+
textDocument: { uri },
133+
position: { line: lineNumber, character },
134+
context: { includeDeclaration: true },
135+
};
136+
137+
const locations = await client.request<ReferenceParams, Location[] | null>(
138+
"textDocument/references",
139+
params,
140+
);
141+
142+
if (!locations || locations.length === 0) {
143+
return { symbolName, references: [] };
144+
}
145+
146+
const refsWithContext: ReferenceWithContext[] = await Promise.all(
147+
locations.map(async (loc) => {
148+
const lineText = await fetchLineText(loc.uri, loc.range.start.line);
149+
return {
150+
...loc,
151+
lineText,
152+
};
153+
}),
154+
);
155+
156+
return { symbolName, references: refsWithContext };
157+
}
158+
159+
export async function findAllReferences(view: EditorView): Promise<boolean> {
160+
const plugin = LSPPlugin.get(view);
161+
if (!plugin) {
162+
return false;
163+
}
164+
165+
const symbolName = getWordAtCursor(view);
166+
const panel = showReferencesPanel({ symbolName });
167+
168+
try {
169+
const result = await fetchReferences(view);
170+
if (result === null) {
171+
panel.setError("Failed to fetch references");
172+
return false;
173+
}
174+
panel.setReferences(result.references);
175+
return true;
176+
} catch (error) {
177+
console.error("Find references failed:", error);
178+
const errorMessage =
179+
error instanceof Error ? error.message : "Unknown error occurred";
180+
panel.setError(errorMessage);
181+
return false;
182+
}
183+
}
184+
185+
export async function findAllReferencesInTab(
186+
view: EditorView,
187+
): Promise<boolean> {
188+
const plugin = LSPPlugin.get(view);
189+
if (!plugin) {
190+
const toast = (globalThis as Record<string, unknown>).toast as
191+
| ((msg: string) => void)
192+
| undefined;
193+
toast?.("Language server not available");
194+
return false;
195+
}
196+
197+
try {
198+
const result = await fetchReferences(view);
199+
if (result === null) {
200+
return false;
201+
}
202+
203+
if (result.references.length === 0) {
204+
const toast = (globalThis as Record<string, unknown>).toast as
205+
| ((msg: string) => void)
206+
| undefined;
207+
toast?.("No references found");
208+
return true;
209+
}
210+
211+
openReferencesTab({
212+
symbolName: result.symbolName,
213+
references: result.references,
214+
});
215+
return true;
216+
} catch (error) {
217+
console.error("Find references in tab failed:", error);
218+
return false;
219+
}
220+
}
221+
222+
export function closeReferencesPanel(): boolean {
223+
const { hideReferencesPanel } = require("components/referencesPanel");
224+
hideReferencesPanel();
225+
return true;
226+
}

0 commit comments

Comments
 (0)