@birdcc/lsp 0.0.1-alpha.0 → 0.0.1-alpha.1
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/package.json +13 -6
- package/.oxfmtrc.json +0 -16
- package/scripts/copy-hover-yaml.mjs +0 -14
- package/src/completion.ts +0 -223
- package/src/definition.ts +0 -50
- package/src/diagnostic.ts +0 -48
- package/src/document-symbol.ts +0 -27
- package/src/hover-docs.ts +0 -223
- package/src/hover-docs.yaml +0 -600
- package/src/hover.ts +0 -122
- package/src/index.ts +0 -8
- package/src/lsp-server.ts +0 -350
- package/src/references.ts +0 -107
- package/src/server.ts +0 -4
- package/src/shared.ts +0 -182
- package/src/symbol-utils.ts +0 -126
- package/src/validation.ts +0 -85
- package/test/hover-docs.test.ts +0 -18
- package/test/lsp.test.ts +0 -304
- package/test/perf-baseline.test.ts +0 -96
- package/test/validation.test.ts +0 -212
- package/tsconfig.json +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@birdcc/lsp",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-only",
|
|
@@ -9,21 +9,28 @@
|
|
|
9
9
|
"email": "npm-dev@birdcc.link",
|
|
10
10
|
"url": "https://github.com/bird-chinese-community/"
|
|
11
11
|
},
|
|
12
|
+
"description": "Language Server Protocol implementation for BIRD2 configuration files.",
|
|
12
13
|
"main": "./dist/index.js",
|
|
13
|
-
"types": "./
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
14
15
|
"exports": {
|
|
15
16
|
".": {
|
|
16
|
-
"types": "./
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
17
18
|
"default": "./dist/index.js"
|
|
18
19
|
}
|
|
19
20
|
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/**",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
20
27
|
"dependencies": {
|
|
21
28
|
"yaml": "^2.8.2",
|
|
22
29
|
"vscode-languageserver": "^9.0.1",
|
|
23
30
|
"vscode-languageserver-textdocument": "^1.0.12",
|
|
24
|
-
"@birdcc/
|
|
25
|
-
"@birdcc/
|
|
26
|
-
"@birdcc/parser": "0.0.1-alpha.
|
|
31
|
+
"@birdcc/core": "0.0.1-alpha.1",
|
|
32
|
+
"@birdcc/linter": "0.0.1-alpha.1",
|
|
33
|
+
"@birdcc/parser": "0.0.1-alpha.1"
|
|
27
34
|
},
|
|
28
35
|
"repository": {
|
|
29
36
|
"type": "git",
|
package/.oxfmtrc.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "../../../node_modules/oxfmt/configuration_schema.json",
|
|
3
|
-
"ignorePatterns": [
|
|
4
|
-
"node_modules/*",
|
|
5
|
-
"package.json",
|
|
6
|
-
"dist/*",
|
|
7
|
-
".turbo/*",
|
|
8
|
-
"target/*",
|
|
9
|
-
"*.lock",
|
|
10
|
-
"pnpm-*.yaml",
|
|
11
|
-
"*-lock.json"
|
|
12
|
-
],
|
|
13
|
-
"printWidth": 80,
|
|
14
|
-
"tabWidth": 2,
|
|
15
|
-
"useTabs": false
|
|
16
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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);
|
package/src/completion.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
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
|
-
};
|
package/src/definition.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
};
|
package/src/diagnostic.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
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
|
-
});
|
package/src/document-symbol.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
};
|
package/src/hover-docs.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
import { parse } from "yaml";
|
|
6
|
-
|
|
7
|
-
export type HoverDocDiffType = "same" | "added" | "modified" | "removed";
|
|
8
|
-
export type HoverDocVersionTag = "v2+" | "v3+" | "v2" | "v2-v3";
|
|
9
|
-
|
|
10
|
-
interface HoverDocYamlEntry {
|
|
11
|
-
readonly keyword: string;
|
|
12
|
-
readonly description: string;
|
|
13
|
-
readonly detail: string;
|
|
14
|
-
readonly diff: HoverDocDiffType;
|
|
15
|
-
readonly version: HoverDocVersionTag;
|
|
16
|
-
readonly anchor?: string;
|
|
17
|
-
readonly anchors?: {
|
|
18
|
-
readonly v2?: string;
|
|
19
|
-
readonly v3?: string;
|
|
20
|
-
};
|
|
21
|
-
readonly notes?: {
|
|
22
|
-
readonly v2?: string;
|
|
23
|
-
readonly v3?: string;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface HoverDocYamlSource {
|
|
28
|
-
readonly version: number;
|
|
29
|
-
readonly baseUrls: {
|
|
30
|
-
readonly v2: string;
|
|
31
|
-
readonly v3: string;
|
|
32
|
-
};
|
|
33
|
-
readonly entries: readonly HoverDocYamlEntry[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const normalizeAnchor = (value: string | undefined): string | undefined => {
|
|
37
|
-
if (typeof value !== "string") {
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const normalized = value.trim().replace(/^#/, "");
|
|
42
|
-
return normalized.length > 0 ? normalized : undefined;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const loadHoverDocYaml = (): HoverDocYamlSource => {
|
|
46
|
-
const hoverDocsPath = path.join(
|
|
47
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
48
|
-
"hover-docs.yaml",
|
|
49
|
-
);
|
|
50
|
-
const raw = readFileSync(hoverDocsPath, "utf8");
|
|
51
|
-
const parsed = parse(raw) as HoverDocYamlSource;
|
|
52
|
-
|
|
53
|
-
if (!parsed || !Array.isArray(parsed.entries) || !parsed.baseUrls) {
|
|
54
|
-
throw new Error("Invalid hover docs yaml structure");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return parsed;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const isDiffType = (value: string): value is HoverDocDiffType =>
|
|
61
|
-
value === "same" ||
|
|
62
|
-
value === "added" ||
|
|
63
|
-
value === "modified" ||
|
|
64
|
-
value === "removed";
|
|
65
|
-
|
|
66
|
-
const isVersionTag = (value: string): value is HoverDocVersionTag =>
|
|
67
|
-
value === "v2+" || value === "v3+" || value === "v2" || value === "v2-v3";
|
|
68
|
-
|
|
69
|
-
const hoverDocSource = loadHoverDocYaml();
|
|
70
|
-
|
|
71
|
-
const VERSION_BASE_URLS = {
|
|
72
|
-
v2: hoverDocSource.baseUrls.v2,
|
|
73
|
-
v3: hoverDocSource.baseUrls.v3,
|
|
74
|
-
} as const;
|
|
75
|
-
|
|
76
|
-
const toCanonicalEntry = (entry: HoverDocYamlEntry): HoverDocYamlEntry => {
|
|
77
|
-
const keyword = entry.keyword.trim().toLowerCase();
|
|
78
|
-
if (keyword.length === 0) {
|
|
79
|
-
throw new Error("Hover keyword must not be empty");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!isDiffType(entry.diff)) {
|
|
83
|
-
throw new Error(`Invalid diff type for keyword '${keyword}'`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!isVersionTag(entry.version)) {
|
|
87
|
-
throw new Error(`Invalid version tag for keyword '${keyword}'`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const anchor = normalizeAnchor(entry.anchor);
|
|
91
|
-
const anchors = entry.anchors
|
|
92
|
-
? {
|
|
93
|
-
...(normalizeAnchor(entry.anchors.v2)
|
|
94
|
-
? { v2: normalizeAnchor(entry.anchors.v2) }
|
|
95
|
-
: {}),
|
|
96
|
-
...(normalizeAnchor(entry.anchors.v3)
|
|
97
|
-
? { v3: normalizeAnchor(entry.anchors.v3) }
|
|
98
|
-
: {}),
|
|
99
|
-
}
|
|
100
|
-
: undefined;
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
(entry.version === "v2+" ||
|
|
104
|
-
entry.version === "v3+" ||
|
|
105
|
-
entry.version === "v2") &&
|
|
106
|
-
!anchor
|
|
107
|
-
) {
|
|
108
|
-
throw new Error(
|
|
109
|
-
`Keyword '${keyword}' requires a single anchor for version '${entry.version}'`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
entry.version === "v2-v3" &&
|
|
115
|
-
!anchor &&
|
|
116
|
-
(!anchors || Object.keys(anchors).length === 0)
|
|
117
|
-
) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
`Keyword '${keyword}' requires anchor or anchors for version 'v2-v3'`,
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
...entry,
|
|
125
|
-
keyword,
|
|
126
|
-
anchor,
|
|
127
|
-
anchors,
|
|
128
|
-
};
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const normalizedEntries = hoverDocSource.entries.map(toCanonicalEntry);
|
|
132
|
-
|
|
133
|
-
const dedupedEntries: HoverDocYamlEntry[] = [];
|
|
134
|
-
const seenKeywords = new Set<string>();
|
|
135
|
-
for (const entry of normalizedEntries) {
|
|
136
|
-
if (seenKeywords.has(entry.keyword)) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
seenKeywords.add(entry.keyword);
|
|
141
|
-
dedupedEntries.push(entry);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const buildDocUrl = (
|
|
145
|
-
version: keyof typeof VERSION_BASE_URLS,
|
|
146
|
-
anchor?: string,
|
|
147
|
-
): string => {
|
|
148
|
-
const baseUrl = VERSION_BASE_URLS[version];
|
|
149
|
-
if (!anchor || anchor.length === 0) {
|
|
150
|
-
return baseUrl;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return `${baseUrl}#${anchor}`;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const buildDocsSection = (entry: HoverDocYamlEntry): string => {
|
|
157
|
-
const lines: string[] = [];
|
|
158
|
-
|
|
159
|
-
if (entry.version === "v2+") {
|
|
160
|
-
lines.push(`- [BIRD v2.18 / v3.2.0](${buildDocUrl("v2", entry.anchor)})`);
|
|
161
|
-
} else if (entry.version === "v3+") {
|
|
162
|
-
lines.push(`- [BIRD v3.2.0](${buildDocUrl("v3", entry.anchor)})`);
|
|
163
|
-
} else if (entry.version === "v2") {
|
|
164
|
-
lines.push(`- [BIRD v2.18](${buildDocUrl("v2", entry.anchor)})`);
|
|
165
|
-
} else {
|
|
166
|
-
const v2Anchor = entry.anchors?.v2 ?? entry.anchor;
|
|
167
|
-
const v3Anchor = entry.anchors?.v3 ?? entry.anchor;
|
|
168
|
-
|
|
169
|
-
if (v2Anchor) {
|
|
170
|
-
lines.push(`- [BIRD v2.18](${buildDocUrl("v2", v2Anchor)})`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (v3Anchor) {
|
|
174
|
-
lines.push(`- [BIRD v3.2.0](${buildDocUrl("v3", v3Anchor)})`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return lines.join("\n");
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const buildNotesSection = (entry: HoverDocYamlEntry): string => {
|
|
182
|
-
if (!entry.notes) {
|
|
183
|
-
return "";
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const lines: string[] = [];
|
|
187
|
-
if (entry.notes.v2) {
|
|
188
|
-
lines.push(`- v2: ${entry.notes.v2}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (entry.notes.v3) {
|
|
192
|
-
lines.push(`- v3: ${entry.notes.v3}`);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (lines.length === 0) {
|
|
196
|
-
return "";
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return `\n\nNotes:\n${lines.join("\n")}`;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const toHoverMarkdown = (entry: HoverDocYamlEntry): string => {
|
|
203
|
-
return (
|
|
204
|
-
[
|
|
205
|
-
`### ${entry.description}`,
|
|
206
|
-
"",
|
|
207
|
-
entry.detail,
|
|
208
|
-
"",
|
|
209
|
-
`Diff: \`${entry.diff}\``,
|
|
210
|
-
`Version: \`${entry.version}\``,
|
|
211
|
-
"Docs:",
|
|
212
|
-
buildDocsSection(entry),
|
|
213
|
-
].join("\n") + buildNotesSection(entry)
|
|
214
|
-
);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
export const HOVER_KEYWORD_DOCS: Record<string, string> = Object.fromEntries(
|
|
218
|
-
dedupedEntries.map((entry) => [entry.keyword, toHoverMarkdown(entry)]),
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
export const HOVER_KEYWORDS: readonly string[] = Object.freeze(
|
|
222
|
-
dedupedEntries.map((entry) => entry.keyword),
|
|
223
|
-
);
|