@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.
Files changed (77) hide show
  1. package/.oxfmtrc.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +343 -0
  4. package/dist/completion.d.ts +8 -0
  5. package/dist/completion.d.ts.map +1 -0
  6. package/dist/completion.js +137 -0
  7. package/dist/completion.js.map +1 -0
  8. package/dist/definition.d.ts +5 -0
  9. package/dist/definition.d.ts.map +1 -0
  10. package/dist/definition.js +21 -0
  11. package/dist/definition.js.map +1 -0
  12. package/dist/diagnostic.d.ts +6 -0
  13. package/dist/diagnostic.d.ts.map +1 -0
  14. package/dist/diagnostic.js +38 -0
  15. package/dist/diagnostic.js.map +1 -0
  16. package/dist/document-symbol.d.ts +4 -0
  17. package/dist/document-symbol.d.ts.map +1 -0
  18. package/dist/document-symbol.js +20 -0
  19. package/dist/document-symbol.js.map +1 -0
  20. package/dist/hover-docs.d.ts +5 -0
  21. package/dist/hover-docs.d.ts.map +1 -0
  22. package/dist/hover-docs.js +141 -0
  23. package/dist/hover-docs.js.map +1 -0
  24. package/dist/hover-docs.yaml +600 -0
  25. package/dist/hover.d.ts +5 -0
  26. package/dist/hover.d.ts.map +1 -0
  27. package/dist/hover.js +81 -0
  28. package/dist/hover.js.map +1 -0
  29. package/dist/index.d.ts +9 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +8 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/lsp-server.d.ts +3 -0
  34. package/dist/lsp-server.d.ts.map +1 -0
  35. package/dist/lsp-server.js +250 -0
  36. package/dist/lsp-server.js.map +1 -0
  37. package/dist/references.d.ts +5 -0
  38. package/dist/references.d.ts.map +1 -0
  39. package/dist/references.js +48 -0
  40. package/dist/references.js.map +1 -0
  41. package/dist/server.d.ts +3 -0
  42. package/dist/server.d.ts.map +1 -0
  43. package/dist/server.js +4 -0
  44. package/dist/server.js.map +1 -0
  45. package/dist/shared.d.ts +17 -0
  46. package/dist/shared.d.ts.map +1 -0
  47. package/dist/shared.js +150 -0
  48. package/dist/shared.js.map +1 -0
  49. package/dist/symbol-utils.d.ts +17 -0
  50. package/dist/symbol-utils.d.ts.map +1 -0
  51. package/dist/symbol-utils.js +84 -0
  52. package/dist/symbol-utils.js.map +1 -0
  53. package/dist/validation.d.ts +21 -0
  54. package/dist/validation.d.ts.map +1 -0
  55. package/dist/validation.js +47 -0
  56. package/dist/validation.js.map +1 -0
  57. package/package.json +45 -0
  58. package/scripts/copy-hover-yaml.mjs +14 -0
  59. package/src/completion.ts +223 -0
  60. package/src/definition.ts +50 -0
  61. package/src/diagnostic.ts +48 -0
  62. package/src/document-symbol.ts +27 -0
  63. package/src/hover-docs.ts +223 -0
  64. package/src/hover-docs.yaml +600 -0
  65. package/src/hover.ts +122 -0
  66. package/src/index.ts +8 -0
  67. package/src/lsp-server.ts +350 -0
  68. package/src/references.ts +107 -0
  69. package/src/server.ts +4 -0
  70. package/src/shared.ts +182 -0
  71. package/src/symbol-utils.ts +126 -0
  72. package/src/validation.ts +85 -0
  73. package/test/hover-docs.test.ts +18 -0
  74. package/test/lsp.test.ts +304 -0
  75. package/test/perf-baseline.test.ts +96 -0
  76. package/test/validation.test.ts +212 -0
  77. package/tsconfig.json +8 -0
@@ -0,0 +1,84 @@
1
+ const addToMapList = (map, key, value) => {
2
+ const existing = map.get(key);
3
+ if (existing) {
4
+ existing.push(value);
5
+ return;
6
+ }
7
+ map.set(key, [value]);
8
+ };
9
+ export const containsPosition = (range, position) => {
10
+ const line = position.line + 1;
11
+ const column = position.character + 1;
12
+ if (line < range.line || line > range.endLine) {
13
+ return false;
14
+ }
15
+ if (line === range.line && column < range.column) {
16
+ return false;
17
+ }
18
+ if (line === range.endLine && column > range.endColumn) {
19
+ return false;
20
+ }
21
+ return true;
22
+ };
23
+ export const toLocation = (symbol) => ({
24
+ uri: symbol.uri,
25
+ range: {
26
+ start: {
27
+ line: Math.max(0, symbol.line - 1),
28
+ character: Math.max(0, symbol.column - 1),
29
+ },
30
+ end: {
31
+ line: Math.max(0, symbol.endLine - 1),
32
+ character: Math.max(0, symbol.endColumn - 1),
33
+ },
34
+ },
35
+ });
36
+ export const extractWordAtPosition = (text, position) => {
37
+ const lineText = text.split(/\r?\n/)[position.line] ?? "";
38
+ if (position.character < 0 || position.character > lineText.length) {
39
+ return "";
40
+ }
41
+ let start = position.character;
42
+ while (start > 0 && /[A-Za-z0-9_]/.test(lineText[start - 1] ?? "")) {
43
+ start -= 1;
44
+ }
45
+ let end = position.character;
46
+ while (end < lineText.length && /[A-Za-z0-9_]/.test(lineText[end] ?? "")) {
47
+ end += 1;
48
+ }
49
+ return lineText.slice(start, end).trim();
50
+ };
51
+ export const dedupeLocations = (locations) => {
52
+ const seen = new Set();
53
+ const output = [];
54
+ for (const location of locations) {
55
+ const key = [
56
+ location.uri,
57
+ location.range.start.line,
58
+ location.range.start.character,
59
+ location.range.end.line,
60
+ location.range.end.character,
61
+ ].join(":");
62
+ if (seen.has(key)) {
63
+ continue;
64
+ }
65
+ seen.add(key);
66
+ output.push(location);
67
+ }
68
+ return output;
69
+ };
70
+ export const createSymbolLookupIndex = (definitions, references) => {
71
+ const definitionsByName = new Map();
72
+ const referencesByName = new Map();
73
+ for (const definition of definitions) {
74
+ addToMapList(definitionsByName, definition.name.toLowerCase(), definition);
75
+ }
76
+ for (const reference of references) {
77
+ addToMapList(referencesByName, reference.name.toLowerCase(), reference);
78
+ }
79
+ return {
80
+ definitionsByName,
81
+ referencesByName,
82
+ };
83
+ };
84
+ //# sourceMappingURL=symbol-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symbol-utils.js","sourceRoot":"","sources":["../src/symbol-utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export interface ValidationDocument {
2
+ uri: string;
3
+ version: number;
4
+ getText(): string;
5
+ }
6
+ export interface ValidationPublishPayload<TDiagnostic> {
7
+ uri: string;
8
+ version?: number;
9
+ diagnostics: TDiagnostic[];
10
+ }
11
+ export interface ValidationSchedulerOptions<TDocument extends ValidationDocument, TDiagnostic> {
12
+ debounceMs: number;
13
+ validate(document: TDocument): Promise<TDiagnostic[]>;
14
+ publish(payload: ValidationPublishPayload<TDiagnostic>): void;
15
+ }
16
+ export interface ValidationScheduler<TDocument extends ValidationDocument> {
17
+ schedule(document: TDocument): void;
18
+ close(uri: string): void;
19
+ }
20
+ export declare const createValidationScheduler: <TDocument extends ValidationDocument, TDiagnostic>(options: ValidationSchedulerOptions<TDocument, TDiagnostic>) => ValidationScheduler<TDocument>;
21
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,IAAI,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB,CAAC,WAAW;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,WAAW,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B,CACzC,SAAS,SAAS,kBAAkB,EACpC,WAAW;IAEX,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,QAAQ,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACtD,OAAO,CAAC,OAAO,EAAE,wBAAwB,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC;CAC/D;AAED,MAAM,WAAW,mBAAmB,CAAC,SAAS,SAAS,kBAAkB;IACvE,QAAQ,CAAC,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAC;IACpC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,eAAO,MAAM,yBAAyB,GACpC,SAAS,SAAS,kBAAkB,EACpC,WAAW,EAEX,SAAS,0BAA0B,CAAC,SAAS,EAAE,WAAW,CAAC,KAC1D,mBAAmB,CAAC,SAAS,CAqD/B,CAAC"}
@@ -0,0 +1,47 @@
1
+ export const createValidationScheduler = (options) => {
2
+ const pendingTimers = new Map();
3
+ const latestTicketByUri = new Map();
4
+ const latestVersionByUri = new Map();
5
+ let nextTicket = 0;
6
+ const clearPending = (uri) => {
7
+ const pendingTimer = pendingTimers.get(uri);
8
+ if (!pendingTimer) {
9
+ return;
10
+ }
11
+ clearTimeout(pendingTimer);
12
+ pendingTimers.delete(uri);
13
+ };
14
+ const runValidation = async (document) => {
15
+ const uri = document.uri;
16
+ const ticket = ++nextTicket;
17
+ latestTicketByUri.set(uri, ticket);
18
+ latestVersionByUri.set(uri, document.version);
19
+ const diagnostics = await options.validate(document);
20
+ if (latestTicketByUri.get(uri) !== ticket) {
21
+ return;
22
+ }
23
+ options.publish({ uri, version: document.version, diagnostics });
24
+ };
25
+ return {
26
+ schedule: (document) => {
27
+ clearPending(document.uri);
28
+ const timer = setTimeout(() => {
29
+ pendingTimers.delete(document.uri);
30
+ void runValidation(document);
31
+ }, options.debounceMs);
32
+ pendingTimers.set(document.uri, timer);
33
+ },
34
+ close: (uri) => {
35
+ clearPending(uri);
36
+ latestTicketByUri.delete(uri);
37
+ const version = latestVersionByUri.get(uri);
38
+ latestVersionByUri.delete(uri);
39
+ options.publish({
40
+ uri,
41
+ version,
42
+ diagnostics: [],
43
+ });
44
+ },
45
+ };
46
+ };
47
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AA0BA,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAIvC,OAA2D,EAC3B,EAAE;IAClC,MAAM,aAAa,GAAG,IAAI,GAAG,EAAyC,CAAC;IACvE,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACpD,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACrD,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,MAAM,YAAY,GAAG,CAAC,GAAW,EAAQ,EAAE;QACzC,MAAM,YAAY,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5C,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QAED,YAAY,CAAC,YAAY,CAAC,CAAC;QAC3B,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,KAAK,EAAE,QAAmB,EAAiB,EAAE;QACjE,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC;QACzB,MAAM,MAAM,GAAG,EAAE,UAAU,CAAC;QAC5B,iBAAiB,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACnC,kBAAkB,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QAE9C,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACrD,IAAI,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,MAAM,EAAE,CAAC;YAC1C,OAAO;QACT,CAAC;QAED,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;IACnE,CAAC,CAAC;IAEF,OAAO;QACL,QAAQ,EAAE,CAAC,QAAmB,EAAQ,EAAE;YACtC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YAE3B,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;gBACnC,KAAK,aAAa,CAAC,QAAQ,CAAC,CAAC;YAC/B,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;YAEvB,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACzC,CAAC;QACD,KAAK,EAAE,CAAC,GAAW,EAAQ,EAAE;YAC3B,YAAY,CAAC,GAAG,CAAC,CAAC;YAClB,iBAAiB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC9B,MAAM,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC5C,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC/B,OAAO,CAAC,OAAO,CAAC;gBACd,GAAG;gBACH,OAAO;gBACP,WAAW,EAAE,EAAE;aAChB,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@birdcc/lsp",
3
+ "version": "0.0.1-alpha.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "GPL-3.0-only",
7
+ "author": {
8
+ "name": "BIRD Chinese Community",
9
+ "email": "npm-dev@birdcc.link",
10
+ "url": "https://github.com/bird-chinese-community/"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "types": "./src/index.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./src/index.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "dependencies": {
21
+ "yaml": "^2.8.2",
22
+ "vscode-languageserver": "^9.0.1",
23
+ "vscode-languageserver-textdocument": "^1.0.12",
24
+ "@birdcc/linter": "0.0.1-alpha.0",
25
+ "@birdcc/core": "0.0.1-alpha.0",
26
+ "@birdcc/parser": "0.0.1-alpha.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/bird-chinese-community/BIRD-LSP",
31
+ "directory": "packages/@birdcc/lsp"
32
+ },
33
+ "homepage": "https://github.com/bird-chinese-community/BIRD-LSP/blob/main/packages/%40birdcc/lsp/README.md",
34
+ "bugs": {
35
+ "url": "https://github.com/bird-chinese-community/BIRD-LSP/issues"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json && node scripts/copy-hover-yaml.mjs",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "lint": "oxlint --deny-warnings src test",
41
+ "test": "vitest run",
42
+ "format": "oxfmt .",
43
+ "coverage": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=text-summary --coverage.reporter=json-summary"
44
+ }
45
+ }
@@ -0,0 +1,14 @@
1
+ import { copyFile, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const packageRoot = path.resolve(__dirname, "..");
8
+
9
+ const sourcePath = path.join(packageRoot, "src", "hover-docs.yaml");
10
+ const targetDir = path.join(packageRoot, "dist");
11
+ const targetPath = path.join(targetDir, "hover-docs.yaml");
12
+
13
+ await mkdir(targetDir, { recursive: true });
14
+ await copyFile(sourcePath, targetPath);
@@ -0,0 +1,223 @@
1
+ import {
2
+ CompletionItemKind,
3
+ InsertTextFormat,
4
+ type CompletionItem,
5
+ } from "vscode-languageserver/node.js";
6
+ import type { ParsedBirdDocument } from "@birdcc/parser";
7
+ import { declarationMetadata, KEYWORD_DOCS } from "./shared.js";
8
+
9
+ const COMPLETION_KEYWORDS = [
10
+ "protocol",
11
+ "template",
12
+ "filter",
13
+ "function",
14
+ "define",
15
+ "include",
16
+ "import",
17
+ "export",
18
+ "neighbor",
19
+ "local as",
20
+ "router id",
21
+ "table",
22
+ "ipv4",
23
+ "ipv6",
24
+ ];
25
+
26
+ interface CompletionSnippet {
27
+ label: string;
28
+ detail: string;
29
+ documentation: string;
30
+ insertText: string;
31
+ }
32
+
33
+ const COMPLETION_SNIPPETS: CompletionSnippet[] = [
34
+ {
35
+ label: 'include "..."',
36
+ detail: "BIRD snippet",
37
+ documentation: "Insert include statement.",
38
+ insertText: 'include "${1:path/to/file.conf}";',
39
+ },
40
+ {
41
+ label: "define NAME = value;",
42
+ detail: "BIRD snippet",
43
+ documentation: "Insert define statement.",
44
+ insertText: "define ${1:NAME} = ${2:value};",
45
+ },
46
+ {
47
+ label: "router id 1.1.1.1;",
48
+ detail: "BIRD snippet",
49
+ documentation: "Insert router id statement.",
50
+ insertText: "router id ${1:1.1.1.1};",
51
+ },
52
+ {
53
+ label: "protocol bgp ...",
54
+ detail: "BIRD snippet",
55
+ documentation: "Insert minimal BGP protocol block.",
56
+ insertText:
57
+ "protocol bgp ${1:name} {\n neighbor ${2:192.0.2.1} as ${3:65001};\n local as ${4:65000};\n}",
58
+ },
59
+ ];
60
+
61
+ export interface CompletionContextOptions {
62
+ linePrefix?: string;
63
+ additionalDeclarations?: ParsedBirdDocument["program"]["declarations"];
64
+ }
65
+
66
+ const isFromTemplateContext = (linePrefix: string): boolean =>
67
+ /\bfrom\s+[A-Za-z_][A-Za-z0-9_]*$/i.test(linePrefix) ||
68
+ /\bfrom\s*$/i.test(linePrefix);
69
+
70
+ const isIncludePathContext = (linePrefix: string): boolean =>
71
+ /\binclude\s+["'][^"']*$/i.test(linePrefix);
72
+
73
+ const isFilterContext = (linePrefix: string): boolean =>
74
+ /\b(?:import|export)\s+filter\s+[A-Za-z_][A-Za-z0-9_]*$/i.test(linePrefix) ||
75
+ /\b(?:import|export)\s+filter\s*$/i.test(linePrefix);
76
+
77
+ const isTableContext = (linePrefix: string): boolean =>
78
+ /\btable\s+[A-Za-z_][A-Za-z0-9_]*$/i.test(linePrefix) ||
79
+ /\btable\s*$/i.test(linePrefix);
80
+
81
+ const allDeclarations = (
82
+ parsed: ParsedBirdDocument,
83
+ options: CompletionContextOptions,
84
+ ): ParsedBirdDocument["program"]["declarations"] => {
85
+ const additional = options.additionalDeclarations ?? [];
86
+ if (additional.length === 0) {
87
+ return parsed.program.declarations;
88
+ }
89
+
90
+ return [...parsed.program.declarations, ...additional];
91
+ };
92
+
93
+ const keywordCompletionItems = (): CompletionItem[] =>
94
+ COMPLETION_KEYWORDS.map((keyword) => ({
95
+ label: keyword,
96
+ kind: CompletionItemKind.Keyword,
97
+ detail: "BIRD keyword",
98
+ documentation: KEYWORD_DOCS[keyword] ?? "",
99
+ }));
100
+
101
+ const snippetCompletionItems = (): CompletionItem[] =>
102
+ COMPLETION_SNIPPETS.map((snippet) => ({
103
+ label: snippet.label,
104
+ kind: CompletionItemKind.Snippet,
105
+ detail: snippet.detail,
106
+ documentation: snippet.documentation,
107
+ insertText: snippet.insertText,
108
+ insertTextFormat: InsertTextFormat.Snippet,
109
+ }));
110
+
111
+ const includePathCompletionItems = (
112
+ declarations: ParsedBirdDocument["program"]["declarations"],
113
+ options: { quoteWrapped: boolean },
114
+ ): CompletionItem[] => {
115
+ const paths = new Set<string>();
116
+
117
+ for (const declaration of declarations) {
118
+ if (declaration.kind !== "include") {
119
+ continue;
120
+ }
121
+
122
+ if (declaration.path.length > 0) {
123
+ paths.add(declaration.path);
124
+ }
125
+ }
126
+
127
+ return Array.from(paths).map((path) => ({
128
+ label: path,
129
+ kind: CompletionItemKind.File,
130
+ detail: "include path",
131
+ insertText: options.quoteWrapped ? path : `"${path}"`,
132
+ }));
133
+ };
134
+
135
+ const collectDeclarationCompletionItems = (
136
+ declarations: ParsedBirdDocument["program"]["declarations"],
137
+ predicate: (
138
+ declaration: ParsedBirdDocument["program"]["declarations"][number],
139
+ ) => boolean,
140
+ ): CompletionItem[] => {
141
+ const items: CompletionItem[] = [];
142
+ const seen = new Set<string>();
143
+
144
+ for (const declaration of declarations) {
145
+ if (!predicate(declaration)) {
146
+ continue;
147
+ }
148
+
149
+ const metadata = declarationMetadata(declaration);
150
+ if (!metadata?.completionLabel || seen.has(metadata.completionLabel)) {
151
+ continue;
152
+ }
153
+
154
+ seen.add(metadata.completionLabel);
155
+ items.push({
156
+ label: metadata.completionLabel,
157
+ kind: metadata.completionKind ?? CompletionItemKind.Reference,
158
+ detail: metadata.completionDetail ?? metadata.detail,
159
+ });
160
+ }
161
+
162
+ return items;
163
+ };
164
+
165
+ const templateCompletionItems = (
166
+ declarations: ParsedBirdDocument["program"]["declarations"],
167
+ ): CompletionItem[] =>
168
+ collectDeclarationCompletionItems(
169
+ declarations,
170
+ (declaration) => declaration.kind === "template",
171
+ );
172
+
173
+ const filterCompletionItems = (
174
+ declarations: ParsedBirdDocument["program"]["declarations"],
175
+ ): CompletionItem[] =>
176
+ collectDeclarationCompletionItems(
177
+ declarations,
178
+ (declaration) => declaration.kind === "filter",
179
+ );
180
+
181
+ const tableCompletionItems = (
182
+ declarations: ParsedBirdDocument["program"]["declarations"],
183
+ ): CompletionItem[] =>
184
+ collectDeclarationCompletionItems(
185
+ declarations,
186
+ (declaration) => declaration.kind === "table",
187
+ );
188
+
189
+ const declarationCompletionItems = (
190
+ declarations: ParsedBirdDocument["program"]["declarations"],
191
+ ): CompletionItem[] => {
192
+ return collectDeclarationCompletionItems(declarations, () => true);
193
+ };
194
+
195
+ export const createCompletionItemsFromParsed = (
196
+ parsed: ParsedBirdDocument,
197
+ options: CompletionContextOptions = {},
198
+ ): CompletionItem[] => {
199
+ const linePrefix = options.linePrefix ?? "";
200
+ const declarations = allDeclarations(parsed, options);
201
+
202
+ if (isIncludePathContext(linePrefix)) {
203
+ return includePathCompletionItems(declarations, { quoteWrapped: true });
204
+ }
205
+
206
+ if (isFromTemplateContext(linePrefix)) {
207
+ return templateCompletionItems(declarations);
208
+ }
209
+
210
+ if (isFilterContext(linePrefix)) {
211
+ return filterCompletionItems(declarations);
212
+ }
213
+
214
+ if (isTableContext(linePrefix)) {
215
+ return tableCompletionItems(declarations);
216
+ }
217
+
218
+ return [
219
+ ...keywordCompletionItems(),
220
+ ...snippetCompletionItems(),
221
+ ...declarationCompletionItems(declarations),
222
+ ];
223
+ };
@@ -0,0 +1,50 @@
1
+ import type { SymbolTable } from "@birdcc/core";
2
+ import type { Location, Position } from "vscode-languageserver/node.js";
3
+ import {
4
+ containsPosition,
5
+ createSymbolLookupIndex,
6
+ dedupeLocations,
7
+ extractWordAtPosition,
8
+ toLocation,
9
+ } from "./symbol-utils.js";
10
+
11
+ /** Resolves symbol definition locations from a merged cross-file symbol table. */
12
+ export const createDefinitionLocations = (
13
+ symbolTable: SymbolTable,
14
+ uri: string,
15
+ position: Position,
16
+ sourceText: string,
17
+ ): Location[] => {
18
+ const index = createSymbolLookupIndex(
19
+ symbolTable.definitions,
20
+ symbolTable.references,
21
+ );
22
+
23
+ const reference = symbolTable.references.find(
24
+ (item) => item.uri === uri && containsPosition(item, position),
25
+ );
26
+
27
+ if (reference) {
28
+ return dedupeLocations(
29
+ (index.definitionsByName.get(reference.name.toLowerCase()) ?? [])
30
+ .filter((definition) => definition.kind === reference.kind)
31
+ .map(toLocation),
32
+ );
33
+ }
34
+
35
+ const definition = symbolTable.definitions.find(
36
+ (item) => item.uri === uri && containsPosition(item, position),
37
+ );
38
+ if (definition) {
39
+ return [toLocation(definition)];
40
+ }
41
+
42
+ const name = extractWordAtPosition(sourceText, position);
43
+ if (!name) {
44
+ return [];
45
+ }
46
+
47
+ return dedupeLocations(
48
+ (index.definitionsByName.get(name.toLowerCase()) ?? []).map(toLocation),
49
+ );
50
+ };
@@ -0,0 +1,48 @@
1
+ import {
2
+ DiagnosticSeverity,
3
+ type Diagnostic,
4
+ } from "vscode-languageserver/node.js";
5
+ import type { BirdDiagnostic } from "@birdcc/core";
6
+
7
+ const toLspSeverity = (
8
+ severity: BirdDiagnostic["severity"],
9
+ ): DiagnosticSeverity => {
10
+ if (severity === "error") {
11
+ return DiagnosticSeverity.Error;
12
+ }
13
+
14
+ if (severity === "warning") {
15
+ return DiagnosticSeverity.Warning;
16
+ }
17
+
18
+ return DiagnosticSeverity.Information;
19
+ };
20
+
21
+ /** Maps Bird diagnostic schema into LSP Diagnostic schema. */
22
+ export const toLspDiagnostic = (diagnostic: BirdDiagnostic): Diagnostic => ({
23
+ code: diagnostic.code,
24
+ message: diagnostic.message,
25
+ severity: toLspSeverity(diagnostic.severity),
26
+ source: diagnostic.source,
27
+ range: {
28
+ start: {
29
+ line: Math.max(diagnostic.range.line - 1, 0),
30
+ character: Math.max(diagnostic.range.column - 1, 0),
31
+ },
32
+ end: {
33
+ line: Math.max(diagnostic.range.endLine - 1, 0),
34
+ character: Math.max(diagnostic.range.endColumn - 1, 0),
35
+ },
36
+ },
37
+ });
38
+
39
+ export const toInternalErrorDiagnostic = (error: unknown): Diagnostic => ({
40
+ code: "lsp/internal-error",
41
+ message: error instanceof Error ? error.message : String(error),
42
+ severity: DiagnosticSeverity.Error,
43
+ source: "lsp",
44
+ range: {
45
+ start: { line: 0, character: 0 },
46
+ end: { line: 0, character: 1 },
47
+ },
48
+ });
@@ -0,0 +1,27 @@
1
+ import type { DocumentSymbol } from "vscode-languageserver/node.js";
2
+ import type { ParsedBirdDocument } from "@birdcc/parser";
3
+ import { declarationMetadata, toLspRange } from "./shared.js";
4
+
5
+ export const createDocumentSymbolsFromParsed = (
6
+ parsed: ParsedBirdDocument,
7
+ ): DocumentSymbol[] => {
8
+ const symbols: DocumentSymbol[] = [];
9
+
10
+ for (const declaration of parsed.program.declarations) {
11
+ const metadata = declarationMetadata(declaration);
12
+ if (!metadata) {
13
+ continue;
14
+ }
15
+
16
+ symbols.push({
17
+ name: metadata.symbolName,
18
+ detail: metadata.detail,
19
+ kind: metadata.symbolKind,
20
+ range: toLspRange(declaration),
21
+ selectionRange: toLspRange(metadata.selectionRange),
22
+ children: [],
23
+ });
24
+ }
25
+
26
+ return symbols;
27
+ };