@birdcc/lsp 0.0.1-alpha.0
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/.oxfmtrc.json +16 -0
- package/LICENSE +674 -0
- package/README.md +343 -0
- package/dist/completion.d.ts +8 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +137 -0
- package/dist/completion.js.map +1 -0
- package/dist/definition.d.ts +5 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +21 -0
- package/dist/definition.js.map +1 -0
- package/dist/diagnostic.d.ts +6 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +38 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/document-symbol.d.ts +4 -0
- package/dist/document-symbol.d.ts.map +1 -0
- package/dist/document-symbol.js +20 -0
- package/dist/document-symbol.js.map +1 -0
- package/dist/hover-docs.d.ts +5 -0
- package/dist/hover-docs.d.ts.map +1 -0
- package/dist/hover-docs.js +141 -0
- package/dist/hover-docs.js.map +1 -0
- package/dist/hover-docs.yaml +600 -0
- package/dist/hover.d.ts +5 -0
- package/dist/hover.d.ts.map +1 -0
- package/dist/hover.js +81 -0
- package/dist/hover.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lsp-server.d.ts +3 -0
- package/dist/lsp-server.d.ts.map +1 -0
- package/dist/lsp-server.js +250 -0
- package/dist/lsp-server.js.map +1 -0
- package/dist/references.d.ts +5 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +48 -0
- package/dist/references.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -0
- package/dist/shared.d.ts +17 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +150 -0
- package/dist/shared.js.map +1 -0
- package/dist/symbol-utils.d.ts +17 -0
- package/dist/symbol-utils.d.ts.map +1 -0
- package/dist/symbol-utils.js +84 -0
- package/dist/symbol-utils.js.map +1 -0
- package/dist/validation.d.ts +21 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +47 -0
- package/dist/validation.js.map +1 -0
- package/package.json +45 -0
- package/scripts/copy-hover-yaml.mjs +14 -0
- package/src/completion.ts +223 -0
- package/src/definition.ts +50 -0
- package/src/diagnostic.ts +48 -0
- package/src/document-symbol.ts +27 -0
- package/src/hover-docs.ts +223 -0
- package/src/hover-docs.yaml +600 -0
- package/src/hover.ts +122 -0
- package/src/index.ts +8 -0
- package/src/lsp-server.ts +350 -0
- package/src/references.ts +107 -0
- package/src/server.ts +4 -0
- package/src/shared.ts +182 -0
- package/src/symbol-utils.ts +126 -0
- package/src/validation.ts +85 -0
- package/test/hover-docs.test.ts +18 -0
- package/test/lsp.test.ts +304 -0
- package/test/perf-baseline.test.ts +96 -0
- package/test/validation.test.ts +212 -0
- package/tsconfig.json +8 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createCompletionItemsFromParsed } from "./completion.js";
|
|
2
|
+
export type { CompletionContextOptions } from "./completion.js";
|
|
3
|
+
export { createDefinitionLocations } from "./definition.js";
|
|
4
|
+
export { toInternalErrorDiagnostic, toLspDiagnostic } from "./diagnostic.js";
|
|
5
|
+
export { createDocumentSymbolsFromParsed } from "./document-symbol.js";
|
|
6
|
+
export { createHoverFromParsed } from "./hover.js";
|
|
7
|
+
export { createReferenceLocations } from "./references.js";
|
|
8
|
+
export { startLspServer } from "./lsp-server.js";
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CROSS_FILE_MAX_DEPTH,
|
|
3
|
+
DEFAULT_CROSS_FILE_MAX_FILES,
|
|
4
|
+
resolveCrossFileReferences,
|
|
5
|
+
type SymbolTable,
|
|
6
|
+
} from "@birdcc/core";
|
|
7
|
+
import {
|
|
8
|
+
createConnection,
|
|
9
|
+
type Diagnostic,
|
|
10
|
+
type InitializeResult,
|
|
11
|
+
ProposedFeatures,
|
|
12
|
+
TextDocuments,
|
|
13
|
+
TextDocumentSyncKind,
|
|
14
|
+
} from "vscode-languageserver/node.js";
|
|
15
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
16
|
+
import type { ParsedBirdDocument } from "@birdcc/parser";
|
|
17
|
+
import { parseBirdConfig } from "@birdcc/parser";
|
|
18
|
+
import {
|
|
19
|
+
lintBirdConfig,
|
|
20
|
+
lintResolvedCrossFileGraph,
|
|
21
|
+
type LintResult,
|
|
22
|
+
} from "@birdcc/linter";
|
|
23
|
+
import { createCompletionItemsFromParsed } from "./completion.js";
|
|
24
|
+
import { createDefinitionLocations } from "./definition.js";
|
|
25
|
+
import { createDocumentSymbolsFromParsed } from "./document-symbol.js";
|
|
26
|
+
import { toInternalErrorDiagnostic, toLspDiagnostic } from "./diagnostic.js";
|
|
27
|
+
import { createHoverFromParsed } from "./hover.js";
|
|
28
|
+
import { createReferenceLocations } from "./references.js";
|
|
29
|
+
import { createValidationScheduler } from "./validation.js";
|
|
30
|
+
|
|
31
|
+
const VALIDATION_DEBOUNCE_MS = 120;
|
|
32
|
+
const INCLUDE_MAX_DEPTH = DEFAULT_CROSS_FILE_MAX_DEPTH;
|
|
33
|
+
const INCLUDE_MAX_FILES = DEFAULT_CROSS_FILE_MAX_FILES;
|
|
34
|
+
|
|
35
|
+
interface ParsedCacheEntry {
|
|
36
|
+
version: number;
|
|
37
|
+
parsed: ParsedBirdDocument;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface GraphCacheEntry {
|
|
41
|
+
entryUri: string;
|
|
42
|
+
visitedUris: Set<string>;
|
|
43
|
+
symbolTable: SymbolTable;
|
|
44
|
+
byUri: Record<string, LintResult>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const warmupParserRuntime = async (): Promise<void> => {
|
|
48
|
+
try {
|
|
49
|
+
await lintBirdConfig("");
|
|
50
|
+
} catch {
|
|
51
|
+
// Warmup is best-effort and must not block server startup.
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const flattenAdditionalDeclarations = (
|
|
56
|
+
graph: GraphCacheEntry,
|
|
57
|
+
uri: string,
|
|
58
|
+
): ParsedBirdDocument["program"]["declarations"] => {
|
|
59
|
+
const declarations: ParsedBirdDocument["program"]["declarations"] = [];
|
|
60
|
+
|
|
61
|
+
for (const [itemUri, lintResult] of Object.entries(graph.byUri)) {
|
|
62
|
+
if (itemUri === uri) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
declarations.push(...lintResult.parsed.program.declarations);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return declarations;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Starts the stdio LSP server with async lint validation and last-write-wins scheduling. */
|
|
73
|
+
export const startLspServer = (): void => {
|
|
74
|
+
const connection = createConnection(ProposedFeatures.all);
|
|
75
|
+
const documents = new TextDocuments(TextDocument);
|
|
76
|
+
const parsedByUri = new Map<string, ParsedCacheEntry>();
|
|
77
|
+
const graphByUri = new Map<string, GraphCacheEntry>();
|
|
78
|
+
const publishedUrisByEntry = new Map<string, Set<string>>();
|
|
79
|
+
let hasShutdownBeenRequested = false;
|
|
80
|
+
|
|
81
|
+
void warmupParserRuntime();
|
|
82
|
+
|
|
83
|
+
const clearEntryTracking = (entryUri: string): void => {
|
|
84
|
+
const publishedUris = publishedUrisByEntry.get(entryUri);
|
|
85
|
+
if (publishedUris) {
|
|
86
|
+
for (const uri of publishedUris) {
|
|
87
|
+
connection.sendDiagnostics({ uri, diagnostics: [] });
|
|
88
|
+
}
|
|
89
|
+
publishedUrisByEntry.delete(entryUri);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const [uri, graph] of graphByUri.entries()) {
|
|
93
|
+
if (graph.entryUri === entryUri) {
|
|
94
|
+
graphByUri.delete(uri);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const analyzeDocument = async (
|
|
100
|
+
document: TextDocument,
|
|
101
|
+
options: { publishRelatedDiagnostics: boolean },
|
|
102
|
+
): Promise<{ entryDiagnostics: Diagnostic[]; graph: GraphCacheEntry }> => {
|
|
103
|
+
const openDocuments = documents.all().map((item) => ({
|
|
104
|
+
uri: item.uri,
|
|
105
|
+
text: item.getText(),
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
const crossFile = await resolveCrossFileReferences({
|
|
109
|
+
entryUri: document.uri,
|
|
110
|
+
documents: openDocuments,
|
|
111
|
+
loadFromFileSystem: true,
|
|
112
|
+
maxDepth: INCLUDE_MAX_DEPTH,
|
|
113
|
+
maxFiles: INCLUDE_MAX_FILES,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const lintGraph = await lintResolvedCrossFileGraph(crossFile);
|
|
117
|
+
const visitedUris = new Set(
|
|
118
|
+
crossFile.visitedUris.length > 0 ? crossFile.visitedUris : [document.uri],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const graph: GraphCacheEntry = {
|
|
122
|
+
entryUri: document.uri,
|
|
123
|
+
visitedUris,
|
|
124
|
+
symbolTable: crossFile.symbolTable,
|
|
125
|
+
byUri: lintGraph.byUri,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
for (const [uri, lintResult] of Object.entries(lintGraph.byUri)) {
|
|
129
|
+
const liveDocument = documents.get(uri);
|
|
130
|
+
parsedByUri.set(uri, {
|
|
131
|
+
version: liveDocument?.version ?? -1,
|
|
132
|
+
parsed: lintResult.parsed,
|
|
133
|
+
});
|
|
134
|
+
graphByUri.set(uri, graph);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const diagnosticsByUri = new Map<string, Diagnostic[]>();
|
|
138
|
+
for (const uri of visitedUris) {
|
|
139
|
+
const lintResult = lintGraph.byUri[uri];
|
|
140
|
+
diagnosticsByUri.set(
|
|
141
|
+
uri,
|
|
142
|
+
(lintResult?.diagnostics ?? []).map(toLspDiagnostic),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const entryDiagnostics = diagnosticsByUri.get(document.uri) ?? [];
|
|
147
|
+
if (options.publishRelatedDiagnostics) {
|
|
148
|
+
const previousUris =
|
|
149
|
+
publishedUrisByEntry.get(document.uri) ?? new Set<string>();
|
|
150
|
+
|
|
151
|
+
for (const uri of previousUris) {
|
|
152
|
+
if (visitedUris.has(uri)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
connection.sendDiagnostics({ uri, diagnostics: [] });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const [uri, diagnostics] of diagnosticsByUri) {
|
|
160
|
+
if (uri === document.uri) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
connection.sendDiagnostics({
|
|
165
|
+
uri,
|
|
166
|
+
version: documents.get(uri)?.version,
|
|
167
|
+
diagnostics,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
publishedUrisByEntry.set(document.uri, visitedUris);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { entryDiagnostics, graph };
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const getGraphForDocument = async (
|
|
178
|
+
document: TextDocument,
|
|
179
|
+
): Promise<GraphCacheEntry> => {
|
|
180
|
+
const cached = graphByUri.get(document.uri);
|
|
181
|
+
if (cached) {
|
|
182
|
+
return cached;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const analyzed = await analyzeDocument(document, {
|
|
186
|
+
publishRelatedDiagnostics: false,
|
|
187
|
+
});
|
|
188
|
+
return analyzed.graph;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
connection.onInitialize(
|
|
192
|
+
(): InitializeResult => ({
|
|
193
|
+
capabilities: {
|
|
194
|
+
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
195
|
+
documentSymbolProvider: true,
|
|
196
|
+
hoverProvider: true,
|
|
197
|
+
definitionProvider: true,
|
|
198
|
+
referencesProvider: true,
|
|
199
|
+
completionProvider: {
|
|
200
|
+
resolveProvider: false,
|
|
201
|
+
triggerCharacters: [" ", "."],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const scheduler = createValidationScheduler<TextDocument, Diagnostic>({
|
|
208
|
+
debounceMs: VALIDATION_DEBOUNCE_MS,
|
|
209
|
+
validate: async (textDocument): Promise<Diagnostic[]> => {
|
|
210
|
+
try {
|
|
211
|
+
const analyzed = await analyzeDocument(textDocument, {
|
|
212
|
+
publishRelatedDiagnostics: true,
|
|
213
|
+
});
|
|
214
|
+
return analyzed.entryDiagnostics;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return [toInternalErrorDiagnostic(error)];
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
publish: (payload) => {
|
|
220
|
+
connection.sendDiagnostics(payload);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
documents.onDidOpen((event) => {
|
|
225
|
+
scheduler.schedule(event.document);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
documents.onDidChangeContent((event) => {
|
|
229
|
+
scheduler.schedule(event.document);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
documents.onDidClose((event) => {
|
|
233
|
+
parsedByUri.delete(event.document.uri);
|
|
234
|
+
clearEntryTracking(event.document.uri);
|
|
235
|
+
scheduler.close(event.document.uri);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const getParsedDocument = async (
|
|
239
|
+
document: TextDocument,
|
|
240
|
+
): Promise<ParsedBirdDocument> => {
|
|
241
|
+
const cached = parsedByUri.get(document.uri);
|
|
242
|
+
if (cached && cached.version === document.version) {
|
|
243
|
+
return cached.parsed;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const parsed = await parseBirdConfig(document.getText());
|
|
247
|
+
parsedByUri.set(document.uri, {
|
|
248
|
+
version: document.version,
|
|
249
|
+
parsed,
|
|
250
|
+
});
|
|
251
|
+
return parsed;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
connection.onDocumentSymbol(async (params) => {
|
|
255
|
+
const document = documents.get(params.textDocument.uri);
|
|
256
|
+
if (!document) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const parsed = await getParsedDocument(document);
|
|
261
|
+
return createDocumentSymbolsFromParsed(parsed);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
connection.onHover(async (params) => {
|
|
265
|
+
const document = documents.get(params.textDocument.uri);
|
|
266
|
+
if (!document) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const parsed = await getParsedDocument(document);
|
|
271
|
+
return createHoverFromParsed(parsed, document, params.position);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
connection.onDefinition(async (params) => {
|
|
275
|
+
const document = documents.get(params.textDocument.uri);
|
|
276
|
+
if (!document) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const graph = await getGraphForDocument(document);
|
|
282
|
+
return createDefinitionLocations(
|
|
283
|
+
graph.symbolTable,
|
|
284
|
+
document.uri,
|
|
285
|
+
params.position,
|
|
286
|
+
document.getText(),
|
|
287
|
+
);
|
|
288
|
+
} catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
connection.onReferences(async (params) => {
|
|
294
|
+
const document = documents.get(params.textDocument.uri);
|
|
295
|
+
if (!document) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const graph = await getGraphForDocument(document);
|
|
301
|
+
return createReferenceLocations(
|
|
302
|
+
graph.symbolTable,
|
|
303
|
+
document.uri,
|
|
304
|
+
params.position,
|
|
305
|
+
document.getText(),
|
|
306
|
+
);
|
|
307
|
+
} catch {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
connection.onCompletion(async (params) => {
|
|
313
|
+
const document = documents.get(params.textDocument.uri);
|
|
314
|
+
if (!document) {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const parsed = await getParsedDocument(document);
|
|
319
|
+
const linePrefix = document.getText({
|
|
320
|
+
start: { line: params.position.line, character: 0 },
|
|
321
|
+
end: params.position,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const graph = await getGraphForDocument(document);
|
|
326
|
+
return createCompletionItemsFromParsed(parsed, {
|
|
327
|
+
linePrefix,
|
|
328
|
+
additionalDeclarations: flattenAdditionalDeclarations(
|
|
329
|
+
graph,
|
|
330
|
+
document.uri,
|
|
331
|
+
),
|
|
332
|
+
});
|
|
333
|
+
} catch {
|
|
334
|
+
return createCompletionItemsFromParsed(parsed, { linePrefix });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
connection.onShutdown(() => {
|
|
339
|
+
hasShutdownBeenRequested = true;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
connection.onExit(() => {
|
|
343
|
+
if (!hasShutdownBeenRequested) {
|
|
344
|
+
process.exitCode = 1;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
documents.listen(connection);
|
|
349
|
+
connection.listen();
|
|
350
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SymbolDefinition,
|
|
3
|
+
SymbolReference,
|
|
4
|
+
SymbolTable,
|
|
5
|
+
} from "@birdcc/core";
|
|
6
|
+
import type { Location, Position } from "vscode-languageserver/node.js";
|
|
7
|
+
import {
|
|
8
|
+
containsPosition,
|
|
9
|
+
createSymbolLookupIndex,
|
|
10
|
+
dedupeLocations,
|
|
11
|
+
extractWordAtPosition,
|
|
12
|
+
toLocation,
|
|
13
|
+
} from "./symbol-utils.js";
|
|
14
|
+
|
|
15
|
+
interface SymbolTarget {
|
|
16
|
+
kind?: SymbolDefinition["kind"] | SymbolReference["kind"];
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const findTargetSymbol = (
|
|
21
|
+
symbolTable: SymbolTable,
|
|
22
|
+
index: ReturnType<typeof createSymbolLookupIndex>,
|
|
23
|
+
uri: string,
|
|
24
|
+
position: Position,
|
|
25
|
+
sourceText: string,
|
|
26
|
+
): SymbolTarget | null => {
|
|
27
|
+
const definition = symbolTable.definitions.find(
|
|
28
|
+
(item) => item.uri === uri && containsPosition(item, position),
|
|
29
|
+
);
|
|
30
|
+
if (definition) {
|
|
31
|
+
return { kind: definition.kind, name: definition.name };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const reference = symbolTable.references.find(
|
|
35
|
+
(item) => item.uri === uri && containsPosition(item, position),
|
|
36
|
+
);
|
|
37
|
+
if (reference) {
|
|
38
|
+
return { kind: reference.kind, name: reference.name };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const name = extractWordAtPosition(sourceText, position);
|
|
42
|
+
if (!name) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lowerName = name.toLowerCase();
|
|
47
|
+
|
|
48
|
+
const matchedDefinition = (index.definitionsByName.get(lowerName) ?? [])[0];
|
|
49
|
+
if (matchedDefinition) {
|
|
50
|
+
return { kind: matchedDefinition.kind, name: matchedDefinition.name };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const matchedReference = (index.referencesByName.get(lowerName) ?? [])[0];
|
|
54
|
+
if (matchedReference) {
|
|
55
|
+
return { kind: matchedReference.kind, name: matchedReference.name };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { name };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Resolves cross-file symbol references from a merged symbol table. */
|
|
62
|
+
export const createReferenceLocations = (
|
|
63
|
+
symbolTable: SymbolTable,
|
|
64
|
+
uri: string,
|
|
65
|
+
position: Position,
|
|
66
|
+
sourceText: string,
|
|
67
|
+
): Location[] => {
|
|
68
|
+
const index = createSymbolLookupIndex(
|
|
69
|
+
symbolTable.definitions,
|
|
70
|
+
symbolTable.references,
|
|
71
|
+
);
|
|
72
|
+
const target = findTargetSymbol(
|
|
73
|
+
symbolTable,
|
|
74
|
+
index,
|
|
75
|
+
uri,
|
|
76
|
+
position,
|
|
77
|
+
sourceText,
|
|
78
|
+
);
|
|
79
|
+
if (!target) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const lowerName = target.name.toLowerCase();
|
|
84
|
+
const definitionMatches = (
|
|
85
|
+
index.definitionsByName.get(lowerName) ?? []
|
|
86
|
+
).filter((item) => {
|
|
87
|
+
if (!target.kind) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return item.kind === target.kind;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const referenceMatches = (index.referencesByName.get(lowerName) ?? []).filter(
|
|
95
|
+
(item) => {
|
|
96
|
+
if (!target.kind) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return item.kind === target.kind;
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return dedupeLocations(
|
|
105
|
+
[...definitionMatches, ...referenceMatches].map(toLocation),
|
|
106
|
+
);
|
|
107
|
+
};
|
package/src/server.ts
ADDED
package/src/shared.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CompletionItemKind,
|
|
3
|
+
SymbolKind,
|
|
4
|
+
type Range,
|
|
5
|
+
type Position,
|
|
6
|
+
} from "vscode-languageserver/node.js";
|
|
7
|
+
import type { BirdDeclaration, SourceRange } from "@birdcc/parser";
|
|
8
|
+
import { HOVER_KEYWORD_DOCS } from "./hover-docs.js";
|
|
9
|
+
|
|
10
|
+
const BASE_KEYWORD_DOCS: Record<string, string> = {
|
|
11
|
+
protocol: "Define a protocol instance. Example: `protocol bgp edge { ... }`.",
|
|
12
|
+
template: "Define a reusable protocol template.",
|
|
13
|
+
filter: "Define route filtering logic.",
|
|
14
|
+
function: "Define reusable logic callable from filters.",
|
|
15
|
+
define: "Define a reusable constant. Example: `define ASN = 65001;`.",
|
|
16
|
+
include: "Include another configuration file.",
|
|
17
|
+
table: "Define a routing table resource for protocol/channel usage.",
|
|
18
|
+
import: "Control import policy for routes.",
|
|
19
|
+
export: "Control export policy for routes.",
|
|
20
|
+
neighbor: "Configure protocol neighbor endpoint and ASN.",
|
|
21
|
+
"local as": "Configure local ASN via `local as <asn>;`.",
|
|
22
|
+
"router id": "Set explicit router ID or select from runtime source.",
|
|
23
|
+
ipv4: "IPv4 address family/channel/table scope keyword.",
|
|
24
|
+
ipv6: "IPv6 address family/channel/table scope keyword.",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const KEYWORD_DOCS: Record<string, string> = {
|
|
28
|
+
...BASE_KEYWORD_DOCS,
|
|
29
|
+
...HOVER_KEYWORD_DOCS,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface LspDeclarationMetadata {
|
|
33
|
+
symbolName: string;
|
|
34
|
+
selectionRange: SourceRange;
|
|
35
|
+
symbolKind: SymbolKind;
|
|
36
|
+
detail: string;
|
|
37
|
+
hoverMarkdown: string;
|
|
38
|
+
completionLabel?: string;
|
|
39
|
+
completionKind?: CompletionItemKind;
|
|
40
|
+
completionDetail?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const toLspRange = (range: SourceRange): Range => ({
|
|
44
|
+
start: {
|
|
45
|
+
line: Math.max(range.line - 1, 0),
|
|
46
|
+
character: Math.max(range.column - 1, 0),
|
|
47
|
+
},
|
|
48
|
+
end: {
|
|
49
|
+
line: Math.max(range.endLine - 1, 0),
|
|
50
|
+
character: Math.max(range.endColumn - 1, 0),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const isPositionInRange = (
|
|
55
|
+
position: Position,
|
|
56
|
+
range: SourceRange,
|
|
57
|
+
): boolean => {
|
|
58
|
+
const line = position.line + 1;
|
|
59
|
+
const character = position.character + 1;
|
|
60
|
+
|
|
61
|
+
if (line < range.line || line > range.endLine) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (line === range.line && character < range.column) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (line === range.endLine && character > range.endColumn) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const declarationMetadata = (
|
|
77
|
+
declaration: BirdDeclaration,
|
|
78
|
+
): LspDeclarationMetadata | null => {
|
|
79
|
+
const escapeMarkdownCode = (text: string): string =>
|
|
80
|
+
text.replaceAll("\\", "\\\\").replaceAll("`", "\\`");
|
|
81
|
+
|
|
82
|
+
switch (declaration.kind) {
|
|
83
|
+
case "protocol": {
|
|
84
|
+
const fromTemplate = declaration.fromTemplate
|
|
85
|
+
? ` from \`${escapeMarkdownCode(declaration.fromTemplate)}\``
|
|
86
|
+
: "";
|
|
87
|
+
return {
|
|
88
|
+
symbolName: declaration.name,
|
|
89
|
+
selectionRange: declaration.nameRange,
|
|
90
|
+
symbolKind: SymbolKind.Module,
|
|
91
|
+
detail: `protocol ${declaration.protocolType}`,
|
|
92
|
+
hoverMarkdown: `**protocol** \`${escapeMarkdownCode(declaration.name)}\`\n\nType: \`${escapeMarkdownCode(declaration.protocolType)}\`${fromTemplate}`,
|
|
93
|
+
completionLabel: declaration.name,
|
|
94
|
+
completionKind: CompletionItemKind.Reference,
|
|
95
|
+
completionDetail: `protocol ${declaration.protocolType}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
case "template":
|
|
99
|
+
return {
|
|
100
|
+
symbolName: declaration.name,
|
|
101
|
+
selectionRange: declaration.nameRange,
|
|
102
|
+
symbolKind: SymbolKind.Class,
|
|
103
|
+
detail: `template ${declaration.templateType}`,
|
|
104
|
+
hoverMarkdown: `**template** \`${escapeMarkdownCode(declaration.name)}\`\n\nType: \`${escapeMarkdownCode(declaration.templateType)}\``,
|
|
105
|
+
completionLabel: declaration.name,
|
|
106
|
+
completionKind: CompletionItemKind.Reference,
|
|
107
|
+
completionDetail: `template ${declaration.templateType}`,
|
|
108
|
+
};
|
|
109
|
+
case "filter":
|
|
110
|
+
return {
|
|
111
|
+
symbolName: declaration.name,
|
|
112
|
+
selectionRange: declaration.nameRange,
|
|
113
|
+
symbolKind: SymbolKind.Method,
|
|
114
|
+
detail: "filter",
|
|
115
|
+
hoverMarkdown: `**filter** \`${escapeMarkdownCode(declaration.name)}\``,
|
|
116
|
+
completionLabel: declaration.name,
|
|
117
|
+
completionKind: CompletionItemKind.Reference,
|
|
118
|
+
completionDetail: "filter",
|
|
119
|
+
};
|
|
120
|
+
case "function":
|
|
121
|
+
return {
|
|
122
|
+
symbolName: declaration.name,
|
|
123
|
+
selectionRange: declaration.nameRange,
|
|
124
|
+
symbolKind: SymbolKind.Function,
|
|
125
|
+
detail: "function",
|
|
126
|
+
hoverMarkdown: `**function** \`${escapeMarkdownCode(declaration.name)}\``,
|
|
127
|
+
completionLabel: declaration.name,
|
|
128
|
+
completionKind: CompletionItemKind.Reference,
|
|
129
|
+
completionDetail: "function",
|
|
130
|
+
};
|
|
131
|
+
case "define":
|
|
132
|
+
return {
|
|
133
|
+
symbolName: declaration.name,
|
|
134
|
+
selectionRange: declaration.nameRange,
|
|
135
|
+
symbolKind: SymbolKind.Constant,
|
|
136
|
+
detail: "define",
|
|
137
|
+
hoverMarkdown: `**define** \`${escapeMarkdownCode(declaration.name)}\``,
|
|
138
|
+
completionLabel: declaration.name,
|
|
139
|
+
completionKind: CompletionItemKind.Constant,
|
|
140
|
+
completionDetail: "define",
|
|
141
|
+
};
|
|
142
|
+
case "table":
|
|
143
|
+
return {
|
|
144
|
+
symbolName: declaration.name,
|
|
145
|
+
selectionRange: declaration.nameRange,
|
|
146
|
+
symbolKind: SymbolKind.Object,
|
|
147
|
+
detail: `table ${declaration.tableType}`,
|
|
148
|
+
hoverMarkdown: `**table** \`${escapeMarkdownCode(declaration.name)}\`\n\nType: \`${escapeMarkdownCode(declaration.tableType)}\``,
|
|
149
|
+
completionLabel: declaration.name,
|
|
150
|
+
completionKind: CompletionItemKind.Variable,
|
|
151
|
+
completionDetail: `table ${declaration.tableType}`,
|
|
152
|
+
};
|
|
153
|
+
case "include":
|
|
154
|
+
return {
|
|
155
|
+
symbolName: declaration.path,
|
|
156
|
+
selectionRange: declaration.pathRange,
|
|
157
|
+
symbolKind: SymbolKind.File,
|
|
158
|
+
detail: "include",
|
|
159
|
+
hoverMarkdown: `**include** \`${escapeMarkdownCode(declaration.path)}\``,
|
|
160
|
+
completionLabel: declaration.path,
|
|
161
|
+
completionKind: CompletionItemKind.File,
|
|
162
|
+
completionDetail: "include path",
|
|
163
|
+
};
|
|
164
|
+
case "router-id": {
|
|
165
|
+
const fromSource = declaration.fromSource
|
|
166
|
+
? ` (${declaration.fromSource})`
|
|
167
|
+
: "";
|
|
168
|
+
return {
|
|
169
|
+
symbolName: `router id ${declaration.value}`,
|
|
170
|
+
selectionRange: declaration.valueRange,
|
|
171
|
+
symbolKind: SymbolKind.Property,
|
|
172
|
+
detail: `router-id ${declaration.valueKind}`,
|
|
173
|
+
hoverMarkdown: `**router id** \`${escapeMarkdownCode(declaration.value)}\`${fromSource}`,
|
|
174
|
+
completionLabel: `router id ${declaration.value}`,
|
|
175
|
+
completionKind: CompletionItemKind.Property,
|
|
176
|
+
completionDetail: `router id ${declaration.valueKind}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
default:
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
};
|