Skip to content

Commit 4cbb495

Browse files
committed
feat: LSP document symbols
1 parent 2a67dc0 commit 4cbb495

File tree

2 files changed

+110
-82
lines changed

2 files changed

+110
-82
lines changed

server/src/server.ts

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
TextDocumentSyncKind,
1313
InitializeResult,
1414
Hover,
15-
MarkupKind
15+
MarkupKind,
16+
DocumentSymbol,
17+
SymbolKind,
18+
Range
1619
} from 'vscode-languageserver/node';
1720

1821
import {
@@ -131,6 +134,15 @@ connection.onInitialize((params: InitializeParams) => {
131134
return result;
132135
});
133136

137+
connection.onDocumentSymbol(params => {
138+
const document = documents.get(params.textDocument.uri);
139+
if (!document) {
140+
return [];
141+
}
142+
143+
return parseDocumentSymbols(document);
144+
});
145+
134146
connection.onInitialized(() => {
135147
if (hasConfigurationCapability) {
136148
// Register for all configuration changes.
@@ -455,4 +467,99 @@ connection.onHover(
455467
documents.listen(connection);
456468

457469
// Listen on the connection
458-
connection.listen();
470+
connection.listen();
471+
472+
interface SymbolMatch {
473+
name: string;
474+
detail: string;
475+
kind: SymbolKind;
476+
}
477+
478+
function matchSymbol(line: string): SymbolMatch | undefined {
479+
const defMatch = line.match(/^def\s+([\w\.]+)\s*\(([^)]*)\)\s*:?/);
480+
if (defMatch) {
481+
const [, name, params] = defMatch;
482+
return {
483+
name,
484+
detail: `(${params.trim()})`,
485+
kind: SymbolKind.Function
486+
};
487+
}
488+
489+
const classMatch = line.match(/^class\s+([\w\.]+)\s*(\([^)]*\))?\s*:?/);
490+
if (classMatch) {
491+
const [, name, bases = ''] = classMatch;
492+
return {
493+
name,
494+
detail: bases.trim(),
495+
kind: SymbolKind.Class
496+
};
497+
}
498+
499+
const assignmentMatch = line.match(/^([A-Za-z_]\w*)\s*=\s*.+/);
500+
if (assignmentMatch) {
501+
const [, name] = assignmentMatch;
502+
return {
503+
name,
504+
detail: 'assignment',
505+
kind: SymbolKind.Variable
506+
};
507+
}
508+
509+
return undefined;
510+
}
511+
512+
function buildRange(line: number, indent: number, length: number): Range {
513+
return {
514+
start: { line, character: indent },
515+
end: { line, character: length }
516+
};
517+
}
518+
519+
function parseDocumentSymbols(textDocument: TextDocument): DocumentSymbol[] {
520+
const rootSymbols: DocumentSymbol[] = [];
521+
const stack: Array<{ indent: number; symbol: DocumentSymbol }> = [];
522+
523+
for (let line = 0; line < textDocument.lineCount; line++) {
524+
const lineText = textDocument.getText({
525+
start: { line, character: 0 },
526+
end: { line: line + 1, character: 0 }
527+
});
528+
const content = lineText.replace(/\n$/, '');
529+
const trimmed = content.trim();
530+
531+
if (!trimmed || trimmed.startsWith('#')) {
532+
continue;
533+
}
534+
535+
const symbolMatch = matchSymbol(trimmed);
536+
if (!symbolMatch) {
537+
continue;
538+
}
539+
540+
const indent = content.length - content.trimStart().length;
541+
const range = buildRange(line, indent, content.length);
542+
const symbol: DocumentSymbol = {
543+
name: symbolMatch.name,
544+
detail: symbolMatch.detail,
545+
kind: symbolMatch.kind,
546+
range,
547+
selectionRange: range,
548+
children: []
549+
};
550+
551+
while (stack.length && indent <= stack[stack.length - 1].indent) {
552+
stack.pop();
553+
}
554+
555+
if (stack.length) {
556+
stack[stack.length - 1].symbol.children?.push(symbol);
557+
} else {
558+
rootSymbols.push(symbol);
559+
}
560+
561+
stack.push({ indent, symbol });
562+
}
563+
564+
return rootSymbols;
565+
}

src/extension.ts

Lines changed: 1 addition & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -11,80 +11,6 @@ import {
1111

1212
let client: LanguageClient;
1313

14-
class SageDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
15-
provideDocumentSymbols(document: vscode.TextDocument): vscode.ProviderResult<vscode.DocumentSymbol[]> {
16-
const rootSymbols: vscode.DocumentSymbol[] = [];
17-
const stack: Array<{ indent: number; symbol: vscode.DocumentSymbol }> = [];
18-
19-
for (let line = 0; line < document.lineCount; line++) {
20-
const lineText = document.lineAt(line);
21-
const trimmed = lineText.text.trim();
22-
23-
if (!trimmed || trimmed.startsWith('#')) {
24-
continue;
25-
}
26-
27-
const match = this.matchSymbol(trimmed);
28-
if (!match) {
29-
continue;
30-
}
31-
32-
const indent = lineText.firstNonWhitespaceCharacterIndex;
33-
const { name, detail, kind } = match;
34-
const range = new vscode.Range(line, indent, line, lineText.range.end.character);
35-
const symbol = new vscode.DocumentSymbol(name, detail, kind, range, range);
36-
37-
while (stack.length && indent <= stack[stack.length - 1].indent) {
38-
stack.pop();
39-
}
40-
41-
if (stack.length) {
42-
stack[stack.length - 1].symbol.children.push(symbol);
43-
} else {
44-
rootSymbols.push(symbol);
45-
}
46-
47-
stack.push({ indent, symbol });
48-
}
49-
50-
return rootSymbols;
51-
}
52-
53-
private matchSymbol(line: string): { name: string; detail: string; kind: vscode.SymbolKind } | undefined {
54-
const defMatch = line.match(/^def\s+([\w\.]+)\s*\(([^)]*)\)\s*:?/);
55-
if (defMatch) {
56-
const [, name, params] = defMatch;
57-
return {
58-
name,
59-
detail: `(${params.trim()})`,
60-
kind: vscode.SymbolKind.Function,
61-
};
62-
}
63-
64-
const classMatch = line.match(/^class\s+([\w\.]+)\s*(\([^)]*\))?\s*:?/);
65-
if (classMatch) {
66-
const [, name, bases = ''] = classMatch;
67-
return {
68-
name,
69-
detail: bases.trim(),
70-
kind: vscode.SymbolKind.Class,
71-
};
72-
}
73-
74-
const assignmentMatch = line.match(/^([A-Za-z_]\w*)\s*=\s*.+/);
75-
if (assignmentMatch) {
76-
const [, name] = assignmentMatch;
77-
return {
78-
name,
79-
detail: 'assignment',
80-
kind: vscode.SymbolKind.Variable,
81-
};
82-
}
83-
84-
return undefined;
85-
}
86-
}
87-
8814
export function activate(context: vscode.ExtensionContext) {
8915
// Register the run command first to ensure it's available even if language server fails
9016
const runDisposable = vscode.commands.registerCommand('runsagemathfile.run', () => {
@@ -155,12 +81,7 @@ export function activate(context: vscode.ExtensionContext) {
15581
}
15682
});
15783

158-
const symbolProvider = vscode.languages.registerDocumentSymbolProvider(
159-
{ language: 'sage' },
160-
new SageDocumentSymbolProvider()
161-
);
162-
163-
context.subscriptions.push(runDisposable, restartDisposable, symbolProvider);
84+
context.subscriptions.push(runDisposable, restartDisposable);
16485

16586
// Start the Language Server asynchronously to avoid blocking command registration
16687
startLanguageServer(context).catch(error => {

0 commit comments

Comments
 (0)