Skip to content

Commit 6c2f73a

Browse files
committed
feat: lsp document symbol and fix lsp reference
1 parent 6688fe6 commit 6c2f73a

File tree

9 files changed

+1435
-172
lines changed

9 files changed

+1435
-172
lines changed

src/cm/commandRegistry.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ import {
8686
findAllReferences as acodeFindAllReferences,
8787
findAllReferencesInTab as acodeFindAllReferencesInTab,
8888
} from "cm/lsp/references";
89+
import { showDocumentSymbols } from "components/symbolsPanel";
8990
import toast from "components/toast";
9091
import prompt from "dialogs/prompt";
9192
import actions from "handlers/quickTools";
@@ -1055,6 +1056,17 @@ function registerLspCommands() {
10551056
return true;
10561057
},
10571058
});
1059+
addCommand({
1060+
name: "documentSymbols",
1061+
description: "Go to Symbol in Document...",
1062+
readOnly: true,
1063+
requiresView: true,
1064+
async run(view) {
1065+
const resolvedView = resolveView(view);
1066+
if (!resolvedView) return false;
1067+
return showDocumentSymbols(resolvedView);
1068+
},
1069+
});
10581070
}
10591071

10601072
function registerLintCommands() {

src/cm/lsp/documentSymbols.ts

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
/**
2+
* LSP Document Symbols Extension for CodeMirror
3+
*
4+
* Provides document symbol information (functions, classes, variables, etc.) from language servers.
5+
*/
6+
7+
import { LSPPlugin } from "@codemirror/lsp-client";
8+
import type { EditorView } from "@codemirror/view";
9+
import type {
10+
DocumentSymbol,
11+
Position,
12+
Range,
13+
SymbolInformation,
14+
SymbolKind,
15+
} from "vscode-languageserver-types";
16+
import type { LSPPluginAPI } from "./types";
17+
18+
interface DocumentSymbolParams {
19+
textDocument: { uri: string };
20+
}
21+
22+
export interface ProcessedSymbol {
23+
name: string;
24+
kind: SymbolKind;
25+
kindName: string;
26+
detail?: string;
27+
range: {
28+
startLine: number;
29+
startCharacter: number;
30+
endLine: number;
31+
endCharacter: number;
32+
};
33+
selectionRange: {
34+
startLine: number;
35+
startCharacter: number;
36+
endLine: number;
37+
endCharacter: number;
38+
};
39+
children?: ProcessedSymbol[];
40+
depth: number;
41+
containerName?: string;
42+
}
43+
44+
export interface FlatSymbol {
45+
name: string;
46+
kind: SymbolKind;
47+
kindName: string;
48+
detail?: string;
49+
line: number;
50+
character: number;
51+
endLine: number;
52+
endCharacter: number;
53+
containerName?: string;
54+
depth: number;
55+
}
56+
57+
const SYMBOL_KIND_NAMES: Record<SymbolKind, string> = {
58+
1: "File",
59+
2: "Module",
60+
3: "Namespace",
61+
4: "Package",
62+
5: "Class",
63+
6: "Method",
64+
7: "Property",
65+
8: "Field",
66+
9: "Constructor",
67+
10: "Enum",
68+
11: "Interface",
69+
12: "Function",
70+
13: "Variable",
71+
14: "Constant",
72+
15: "String",
73+
16: "Number",
74+
17: "Boolean",
75+
18: "Array",
76+
19: "Object",
77+
20: "Key",
78+
21: "Null",
79+
22: "EnumMember",
80+
23: "Struct",
81+
24: "Event",
82+
25: "Operator",
83+
26: "TypeParameter",
84+
};
85+
86+
const SYMBOL_KIND_ICONS: Record<SymbolKind, string> = {
87+
1: "insert_drive_file",
88+
2: "view_module",
89+
3: "view_module",
90+
4: "folder",
91+
5: "class",
92+
6: "functions",
93+
7: "label",
94+
8: "label",
95+
9: "functions",
96+
10: "list",
97+
11: "category",
98+
12: "functions",
99+
13: "code",
100+
14: "lock",
101+
15: "text_fields",
102+
16: "pin",
103+
17: "toggle_on",
104+
18: "data_array",
105+
19: "data_object",
106+
20: "key",
107+
21: "not_interested",
108+
22: "list",
109+
23: "data_object",
110+
24: "bolt",
111+
25: "calculate",
112+
26: "text_fields",
113+
};
114+
115+
export function getSymbolKindName(kind: SymbolKind): string {
116+
return SYMBOL_KIND_NAMES[kind] || "Unknown";
117+
}
118+
119+
export function getSymbolKindIcon(kind: SymbolKind): string {
120+
return SYMBOL_KIND_ICONS[kind] || "code";
121+
}
122+
123+
function isDocumentSymbol(
124+
item: DocumentSymbol | SymbolInformation,
125+
): item is DocumentSymbol {
126+
return "selectionRange" in item;
127+
}
128+
129+
function processDocumentSymbol(
130+
symbol: DocumentSymbol,
131+
depth = 0,
132+
containerName?: string,
133+
): ProcessedSymbol {
134+
const processed: ProcessedSymbol = {
135+
name: symbol.name,
136+
kind: symbol.kind,
137+
kindName: getSymbolKindName(symbol.kind),
138+
detail: symbol.detail,
139+
range: {
140+
startLine: symbol.range.start.line,
141+
startCharacter: symbol.range.start.character,
142+
endLine: symbol.range.end.line,
143+
endCharacter: symbol.range.end.character,
144+
},
145+
selectionRange: {
146+
startLine: symbol.selectionRange.start.line,
147+
startCharacter: symbol.selectionRange.start.character,
148+
endLine: symbol.selectionRange.end.line,
149+
endCharacter: symbol.selectionRange.end.character,
150+
},
151+
depth,
152+
containerName,
153+
};
154+
155+
if (symbol.children && symbol.children.length > 0) {
156+
processed.children = symbol.children.map((child) =>
157+
processDocumentSymbol(child, depth + 1, symbol.name),
158+
);
159+
}
160+
161+
return processed;
162+
}
163+
164+
function processSymbolInformation(
165+
symbol: SymbolInformation,
166+
depth = 0,
167+
): ProcessedSymbol {
168+
return {
169+
name: symbol.name,
170+
kind: symbol.kind,
171+
kindName: getSymbolKindName(symbol.kind),
172+
range: {
173+
startLine: symbol.location.range.start.line,
174+
startCharacter: symbol.location.range.start.character,
175+
endLine: symbol.location.range.end.line,
176+
endCharacter: symbol.location.range.end.character,
177+
},
178+
selectionRange: {
179+
startLine: symbol.location.range.start.line,
180+
startCharacter: symbol.location.range.start.character,
181+
endLine: symbol.location.range.end.line,
182+
endCharacter: symbol.location.range.end.character,
183+
},
184+
containerName: symbol.containerName,
185+
depth,
186+
};
187+
}
188+
189+
function flattenSymbols(
190+
symbols: ProcessedSymbol[],
191+
result: FlatSymbol[] = [],
192+
): FlatSymbol[] {
193+
for (const symbol of symbols) {
194+
result.push({
195+
name: symbol.name,
196+
kind: symbol.kind,
197+
kindName: symbol.kindName,
198+
detail: symbol.detail,
199+
line: symbol.selectionRange.startLine,
200+
character: symbol.selectionRange.startCharacter,
201+
endLine: symbol.selectionRange.endLine,
202+
endCharacter: symbol.selectionRange.endCharacter,
203+
containerName: symbol.containerName,
204+
depth: symbol.depth,
205+
});
206+
207+
if (symbol.children) {
208+
flattenSymbols(symbol.children, result);
209+
}
210+
}
211+
212+
return result;
213+
}
214+
215+
export async function fetchDocumentSymbols(
216+
view: EditorView,
217+
): Promise<ProcessedSymbol[] | null> {
218+
const plugin = LSPPlugin.get(view) as LSPPluginAPI | null;
219+
if (!plugin) {
220+
return null;
221+
}
222+
223+
const client = plugin.client;
224+
const capabilities = client.serverCapabilities;
225+
226+
if (!capabilities?.documentSymbolProvider) {
227+
return null;
228+
}
229+
230+
client.sync();
231+
232+
const params: DocumentSymbolParams = {
233+
textDocument: { uri: plugin.uri },
234+
};
235+
236+
try {
237+
const response = await client.request<
238+
DocumentSymbolParams,
239+
(DocumentSymbol | SymbolInformation)[] | null
240+
>("textDocument/documentSymbol", params);
241+
242+
if (!response || response.length === 0) {
243+
return [];
244+
}
245+
246+
if (isDocumentSymbol(response[0])) {
247+
return (response as DocumentSymbol[]).map((sym) =>
248+
processDocumentSymbol(sym),
249+
);
250+
}
251+
252+
return (response as SymbolInformation[]).map((sym) =>
253+
processSymbolInformation(sym),
254+
);
255+
} catch (error) {
256+
console.warn("Failed to fetch document symbols:", error);
257+
return null;
258+
}
259+
}
260+
261+
export async function getDocumentSymbolsFlat(
262+
view: EditorView,
263+
): Promise<FlatSymbol[]> {
264+
const symbols = await fetchDocumentSymbols(view);
265+
if (!symbols) {
266+
return [];
267+
}
268+
269+
return flattenSymbols(symbols);
270+
}
271+
272+
export async function navigateToSymbol(
273+
view: EditorView,
274+
symbol: FlatSymbol | ProcessedSymbol,
275+
): Promise<boolean> {
276+
try {
277+
const doc = view.state.doc;
278+
let targetLine: number;
279+
let targetChar: number;
280+
281+
if ("line" in symbol) {
282+
targetLine = symbol.line;
283+
targetChar = symbol.character;
284+
} else {
285+
targetLine = symbol.selectionRange.startLine;
286+
targetChar = symbol.selectionRange.startCharacter;
287+
}
288+
289+
const lineNumber = targetLine + 1;
290+
if (lineNumber < 1 || lineNumber > doc.lines) {
291+
return false;
292+
}
293+
294+
const line = doc.line(lineNumber);
295+
const pos = Math.min(line.from + targetChar, line.to);
296+
297+
view.dispatch({
298+
selection: { anchor: pos },
299+
scrollIntoView: true,
300+
});
301+
302+
view.focus();
303+
return true;
304+
} catch (error) {
305+
console.warn("Failed to navigate to symbol:", error);
306+
return false;
307+
}
308+
}
309+
310+
export function supportsDocumentSymbols(view: EditorView): boolean {
311+
const plugin = LSPPlugin.get(view) as LSPPluginAPI | null;
312+
if (!plugin?.client.connected) {
313+
return false;
314+
}
315+
316+
return !!plugin.client.serverCapabilities?.documentSymbolProvider;
317+
}
318+
319+
export interface DocumentSymbolsResult {
320+
symbols: ProcessedSymbol[];
321+
flat: FlatSymbol[];
322+
}
323+
324+
export async function getDocumentSymbols(
325+
view: EditorView,
326+
): Promise<DocumentSymbolsResult | null> {
327+
const symbols = await fetchDocumentSymbols(view);
328+
if (symbols === null) {
329+
return null;
330+
}
331+
332+
return {
333+
symbols,
334+
flat: flattenSymbols(symbols),
335+
};
336+
}
337+
338+
export { SymbolKind } from "vscode-languageserver-types";

src/cm/lsp/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ export {
1313
documentHighlightsEditorExtension,
1414
documentHighlightsExtension,
1515
} from "./documentHighlights";
16+
export type {
17+
DocumentSymbolsResult,
18+
FlatSymbol,
19+
ProcessedSymbol,
20+
} from "./documentSymbols";
21+
export {
22+
fetchDocumentSymbols,
23+
getDocumentSymbols,
24+
getDocumentSymbolsFlat,
25+
getSymbolKindIcon,
26+
getSymbolKindName,
27+
navigateToSymbol,
28+
SymbolKind,
29+
supportsDocumentSymbols,
30+
} from "./documentSymbols";
1631
export { registerLspFormatter } from "./formatter";
1732
export type { InlayHintsConfig } from "./inlayHints";
1833
export {

0 commit comments

Comments
 (0)