@rank-lang/lsp 0.3.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/README.md +46 -0
- package/dist/features/completion.d.ts +4 -0
- package/dist/features/completion.js +23 -0
- package/dist/features/definition.d.ts +4 -0
- package/dist/features/definition.js +32 -0
- package/dist/features/diagnostics.d.ts +3 -0
- package/dist/features/diagnostics.js +133 -0
- package/dist/features/hover.d.ts +4 -0
- package/dist/features/hover.js +75 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +57 -0
- package/dist/session.d.ts +14 -0
- package/dist/session.js +167 -0
- package/dist/util/convert.d.ts +6 -0
- package/dist/util/convert.js +40 -0
- package/dist/util/stdlib-hover.d.ts +4 -0
- package/dist/util/stdlib-hover.js +109 -0
- package/dist/util/type-symbols.d.ts +13 -0
- package/dist/util/type-symbols.js +118 -0
- package/package.json +41 -0
- package/src/features/completion.ts +37 -0
- package/src/features/definition.ts +49 -0
- package/src/features/diagnostics.ts +195 -0
- package/src/features/hover.ts +127 -0
- package/src/server.ts +73 -0
- package/src/session.ts +224 -0
- package/src/util/convert.ts +54 -0
- package/src/util/stdlib-hover.ts +178 -0
- package/src/util/type-symbols.ts +165 -0
- package/test/convert.test.ts +46 -0
- package/test/editor-features.test.ts +1167 -0
- package/test/session.test.ts +58 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @rank-lang/lsp
|
|
2
|
+
|
|
3
|
+
Language Server Protocol implementation for Rank.
|
|
4
|
+
|
|
5
|
+
**[Full documentation →](https://rank-lang.com/)**
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
This is a core workspace package used by the VS Code extension and the shared CLI/runtime toolchain.
|
|
10
|
+
|
|
11
|
+
## Surface
|
|
12
|
+
|
|
13
|
+
- executable entrypoint: `rank-lsp`
|
|
14
|
+
- transport: stdio
|
|
15
|
+
- document sync: incremental
|
|
16
|
+
- editor features: diagnostics, hover, go-to-definition, and completion
|
|
17
|
+
|
|
18
|
+
## Diagnostic suppression
|
|
19
|
+
|
|
20
|
+
For intentionally invalid fixtures, the LSP supports declaration-scoped suppression with doc comments:
|
|
21
|
+
|
|
22
|
+
```rank
|
|
23
|
+
/// rank-ignore NAM002 NAM005
|
|
24
|
+
value: Missing<string> = missing_value
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This filters only the published LSP diagnostics for the annotated top-level declaration. The compiler still evaluates the file and still emits the underlying diagnostics for non-LSP tooling.
|
|
28
|
+
|
|
29
|
+
The server is implemented in `src/server.ts` and wires requests into compiler-backed feature handlers through `ProjectSession`.
|
|
30
|
+
|
|
31
|
+
## Running locally
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm run build -w @rank-lang/lsp
|
|
35
|
+
node packages/lsp/dist/server.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
In practice this package is normally launched by `rank-language-support` rather than by hand.
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npm run build -w @rank-lang/lsp
|
|
44
|
+
npm run test -w @rank-lang/lsp
|
|
45
|
+
npm run typecheck -w @rank-lang/lsp
|
|
46
|
+
```
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CompletionItem as LspCompletionItem, TextDocumentPositionParams } from "vscode-languageserver/node.js";
|
|
2
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
|
+
import type { ProjectSession } from "../session.js";
|
|
4
|
+
export declare function handleCompletion(params: TextDocumentPositionParams, session: ProjectSession, document?: TextDocument): Promise<LspCompletionItem[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { completionItemsAtPosition } from "@rank-lang/compiler";
|
|
2
|
+
import { CompletionItemKind } from "vscode-languageserver/node.js";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { toSourcePosition } from "../util/convert.js";
|
|
5
|
+
export async function handleCompletion(params, session, document) {
|
|
6
|
+
const filePath = fileURLToPath(params.textDocument.uri);
|
|
7
|
+
const graph = await session.getModuleGraph(filePath);
|
|
8
|
+
if (!graph)
|
|
9
|
+
return [];
|
|
10
|
+
const mod = session.getLoadedModule(graph, filePath);
|
|
11
|
+
if (!mod)
|
|
12
|
+
return [];
|
|
13
|
+
const position = toSourcePosition(params.position, document);
|
|
14
|
+
const items = completionItemsAtPosition(graph, mod, position);
|
|
15
|
+
return items.map((item) => ({
|
|
16
|
+
label: item.label,
|
|
17
|
+
kind: item.kind === "type"
|
|
18
|
+
? CompletionItemKind.Class
|
|
19
|
+
: item.kind === "keyword"
|
|
20
|
+
? CompletionItemKind.Keyword
|
|
21
|
+
: CompletionItemKind.Variable,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TextDocumentPositionParams, Location } from "vscode-languageserver/node.js";
|
|
2
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
|
+
import type { ProjectSession } from "../session.js";
|
|
4
|
+
export declare function handleDefinition(params: TextDocumentPositionParams, session: ProjectSession, document?: TextDocument): Promise<Location | null>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { findDefinitionAtPosition } from "@rank-lang/compiler";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import { toSourcePosition, toLspRange } from "../util/convert.js";
|
|
4
|
+
import { findTypeSymbolAtPosition } from "../util/type-symbols.js";
|
|
5
|
+
export async function handleDefinition(params, session, document) {
|
|
6
|
+
const filePath = fileURLToPath(params.textDocument.uri);
|
|
7
|
+
const graph = await session.getModuleGraph(filePath);
|
|
8
|
+
if (!graph)
|
|
9
|
+
return null;
|
|
10
|
+
const mod = session.getLoadedModule(graph, filePath);
|
|
11
|
+
if (!mod)
|
|
12
|
+
return null;
|
|
13
|
+
const position = toSourcePosition(params.position, document);
|
|
14
|
+
const definition = findDefinitionAtPosition(graph, mod, position);
|
|
15
|
+
if (definition) {
|
|
16
|
+
return {
|
|
17
|
+
uri: pathToFileURL(definition.filePath).href,
|
|
18
|
+
range: toLspRange(definition.span),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (!document) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const typeSymbol = findTypeSymbolAtPosition(document, params.position, graph, mod);
|
|
25
|
+
if (!typeSymbol) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
uri: pathToFileURL(typeSymbol.filePath).href,
|
|
30
|
+
range: toLspRange(typeSymbol.span),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { resolveModuleImports, resolveModuleReferences, resolveModuleTypeNames, checkModuleTypes, } from "@rank-lang/compiler";
|
|
2
|
+
import { collectRankServerAppSurfaceDiagnostics } from "@rank-lang/server-runtime";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { toLspDiagnostic } from "../util/convert.js";
|
|
5
|
+
function parseIgnoredDiagnosticCodes(documentation) {
|
|
6
|
+
const codes = new Set();
|
|
7
|
+
for (const line of documentation) {
|
|
8
|
+
const match = /^rank-ignore\b(.*)$/u.exec(line.trim());
|
|
9
|
+
if (!match) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
for (const code of (match[1] ?? "").split(/[\s,]+/u).map((value) => value.trim()).filter((value) => value.length > 0)) {
|
|
13
|
+
codes.add(code);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return codes;
|
|
17
|
+
}
|
|
18
|
+
function declarationStartOffset(declaration) {
|
|
19
|
+
switch (declaration.kind) {
|
|
20
|
+
case "ValueBindingDeclaration":
|
|
21
|
+
case "TypeAliasDeclaration":
|
|
22
|
+
return declaration.nameSpan?.start.offset;
|
|
23
|
+
case "DestructuringBindingDeclaration":
|
|
24
|
+
return declaration.fieldSpans
|
|
25
|
+
? Math.min(...Object.values(declaration.fieldSpans).map((span) => span.start.offset))
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function collectDiagnosticSuppressions(loadedModule) {
|
|
30
|
+
const declarationsWithStarts = loadedModule.program.declarations.map((declaration) => ({
|
|
31
|
+
declaration,
|
|
32
|
+
startOffset: declarationStartOffset(declaration),
|
|
33
|
+
}));
|
|
34
|
+
const suppressions = [];
|
|
35
|
+
for (let index = 0; index < declarationsWithStarts.length; index += 1) {
|
|
36
|
+
const current = declarationsWithStarts[index];
|
|
37
|
+
if (!current) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const startOffset = current.startOffset;
|
|
41
|
+
if (startOffset === undefined) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const codes = parseIgnoredDiagnosticCodes(current.declaration.documentation);
|
|
45
|
+
if (codes.size === 0) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const nextStartOffset = declarationsWithStarts
|
|
49
|
+
.slice(index + 1)
|
|
50
|
+
.map((candidate) => candidate.startOffset)
|
|
51
|
+
.find((candidate) => candidate !== undefined);
|
|
52
|
+
suppressions.push({
|
|
53
|
+
startOffset,
|
|
54
|
+
endOffset: nextStartOffset ?? Number.POSITIVE_INFINITY,
|
|
55
|
+
codes,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return suppressions;
|
|
59
|
+
}
|
|
60
|
+
function isSuppressedDiagnostic(diagnostic, suppressions) {
|
|
61
|
+
if (!diagnostic.span) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return suppressions.some((suppression) => (diagnostic.span.start.offset >= suppression.startOffset
|
|
65
|
+
&& diagnostic.span.start.offset < suppression.endOffset
|
|
66
|
+
&& suppression.codes.has(diagnostic.code)));
|
|
67
|
+
}
|
|
68
|
+
function exportedValueDeclaration(loadedModule, name) {
|
|
69
|
+
return loadedModule.program.declarations.find((declaration) => declaration.kind === "ValueBindingDeclaration"
|
|
70
|
+
&& declaration.visibility === "pub"
|
|
71
|
+
&& declaration.name === name);
|
|
72
|
+
}
|
|
73
|
+
function isExecutionContextTypeName(name) {
|
|
74
|
+
return name === "ExecutionContext"
|
|
75
|
+
|| name === "Runtime::ExecutionContext"
|
|
76
|
+
|| name === "std::Runtime::ExecutionContext";
|
|
77
|
+
}
|
|
78
|
+
function isServeExecutionContextTypeAnnotation(typeNode) {
|
|
79
|
+
return !!typeNode
|
|
80
|
+
&& typeNode.kind === "GenericType"
|
|
81
|
+
&& typeNode.arguments.length === 1
|
|
82
|
+
&& isExecutionContextTypeName(typeNode.name);
|
|
83
|
+
}
|
|
84
|
+
function shouldPublishServeDiagnostics(loadedModule) {
|
|
85
|
+
if (exportedValueDeclaration(loadedModule, "config")) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const mainDeclaration = exportedValueDeclaration(loadedModule, "main");
|
|
89
|
+
return !!mainDeclaration
|
|
90
|
+
&& mainDeclaration.value.kind === "FunctionExpression"
|
|
91
|
+
&& (mainDeclaration.value.parameters.length === 0
|
|
92
|
+
|| (mainDeclaration.value.parameters.length === 1
|
|
93
|
+
&& isServeExecutionContextTypeAnnotation(mainDeclaration.value.parameters[0]?.typeAnnotation)));
|
|
94
|
+
}
|
|
95
|
+
export async function publishDiagnostics(connection, session, filePath) {
|
|
96
|
+
const graph = await session.getModuleGraph(filePath);
|
|
97
|
+
if (!graph) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const importDiagnostics = resolveModuleImports(graph);
|
|
101
|
+
const refDiagnostics = resolveModuleReferences(graph);
|
|
102
|
+
const typeDiagnostics = resolveModuleTypeNames(graph);
|
|
103
|
+
const checkDiagnostics = checkModuleTypes(graph, await session.getTypeCheckOptions(filePath, graph));
|
|
104
|
+
const entryModule = graph.modules[graph.entryModulePath];
|
|
105
|
+
const serveDiagnostics = entryModule && shouldPublishServeDiagnostics(entryModule)
|
|
106
|
+
? collectRankServerAppSurfaceDiagnostics(graph, entryModule.filePath)
|
|
107
|
+
: [];
|
|
108
|
+
const allDiagnostics = [
|
|
109
|
+
...graph.diagnostics,
|
|
110
|
+
...importDiagnostics,
|
|
111
|
+
...refDiagnostics,
|
|
112
|
+
...typeDiagnostics,
|
|
113
|
+
...checkDiagnostics,
|
|
114
|
+
...serveDiagnostics,
|
|
115
|
+
];
|
|
116
|
+
// Group diagnostics by source file and publish per-file.
|
|
117
|
+
const byFile = new Map();
|
|
118
|
+
for (const d of allDiagnostics) {
|
|
119
|
+
let list = byFile.get(d.sourcePath);
|
|
120
|
+
if (!list) {
|
|
121
|
+
list = [];
|
|
122
|
+
byFile.set(d.sourcePath, list);
|
|
123
|
+
}
|
|
124
|
+
list.push(d);
|
|
125
|
+
}
|
|
126
|
+
// Publish for the affected file and clear any files with no diagnostics.
|
|
127
|
+
for (const mod of Object.values(graph.modules)) {
|
|
128
|
+
const uri = pathToFileURL(mod.filePath).href;
|
|
129
|
+
const suppressions = collectDiagnosticSuppressions(mod);
|
|
130
|
+
const diags = (byFile.get(mod.filePath) ?? []).filter((diagnostic) => !isSuppressedDiagnostic(diagnostic, suppressions));
|
|
131
|
+
connection.sendDiagnostics({ uri, diagnostics: diags.map(toLspDiagnostic) });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TextDocumentPositionParams, Hover } from "vscode-languageserver/node.js";
|
|
2
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
|
+
import type { ProjectSession } from "../session.js";
|
|
4
|
+
export declare function handleHover(params: TextDocumentPositionParams, session: ProjectSession, document?: TextDocument): Promise<Hover | null>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { hoverInfoAtPosition, queryModuleBindingType, } from "@rank-lang/compiler";
|
|
2
|
+
import { collectRankServerAppSurfaceDiagnostics } from "@rank-lang/server-runtime";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { toSourcePosition } from "../util/convert.js";
|
|
5
|
+
import { findStdlibHoverAtPosition } from "../util/stdlib-hover.js";
|
|
6
|
+
import { findTypeSymbolAtPosition, formatTypeNode } from "../util/type-symbols.js";
|
|
7
|
+
function containsPosition(span, position) {
|
|
8
|
+
return !!span && span.start.offset <= position.offset && position.offset < span.end.offset;
|
|
9
|
+
}
|
|
10
|
+
function exportedValueDeclaration(loadedModule, name) {
|
|
11
|
+
return loadedModule.program.declarations.find((declaration) => declaration.kind === "ValueBindingDeclaration"
|
|
12
|
+
&& declaration.visibility === "pub"
|
|
13
|
+
&& declaration.name === name);
|
|
14
|
+
}
|
|
15
|
+
function serveConfigHoverAtPosition(moduleGraph, loadedModule, position) {
|
|
16
|
+
const entryModule = moduleGraph.modules[moduleGraph.entryModulePath];
|
|
17
|
+
if (!entryModule || entryModule.filePath !== loadedModule.filePath) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const configDeclaration = exportedValueDeclaration(loadedModule, "config");
|
|
21
|
+
if (!configDeclaration || !containsPosition(configDeclaration.nameSpan, position)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const actualType = queryModuleBindingType(moduleGraph, loadedModule, "config", false);
|
|
25
|
+
if (!actualType) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const serveDiagnostics = collectRankServerAppSurfaceDiagnostics(moduleGraph, loadedModule.filePath);
|
|
29
|
+
const lines = [
|
|
30
|
+
"```rank",
|
|
31
|
+
`config: ${actualType}`,
|
|
32
|
+
"```",
|
|
33
|
+
"",
|
|
34
|
+
"Serve config expects `HTTP::AppConfig` or `() -> HTTP::AppConfig`.",
|
|
35
|
+
];
|
|
36
|
+
if (serveDiagnostics.some((diagnostic) => diagnostic.code === "SRV004")) {
|
|
37
|
+
lines.push("", "This binding is invalid for serve-time `pub config`: it must be an object literal or zero-argument function.");
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
export async function handleHover(params, session, document) {
|
|
42
|
+
const filePath = fileURLToPath(params.textDocument.uri);
|
|
43
|
+
const graph = await session.getModuleGraph(filePath);
|
|
44
|
+
if (!graph)
|
|
45
|
+
return null;
|
|
46
|
+
const mod = session.getLoadedModule(graph, filePath);
|
|
47
|
+
if (!mod)
|
|
48
|
+
return null;
|
|
49
|
+
const position = toSourcePosition(params.position, document);
|
|
50
|
+
const serveConfigMarkdown = serveConfigHoverAtPosition(graph, mod, position);
|
|
51
|
+
if (serveConfigMarkdown) {
|
|
52
|
+
return { contents: { kind: "markdown", value: serveConfigMarkdown } };
|
|
53
|
+
}
|
|
54
|
+
const markdown = hoverInfoAtPosition(graph, mod, position);
|
|
55
|
+
if (markdown) {
|
|
56
|
+
return { contents: { kind: "markdown", value: markdown } };
|
|
57
|
+
}
|
|
58
|
+
if (!document) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const stdlibMarkdown = findStdlibHoverAtPosition(document, params.position, mod);
|
|
62
|
+
if (stdlibMarkdown) {
|
|
63
|
+
return { contents: { kind: "markdown", value: stdlibMarkdown } };
|
|
64
|
+
}
|
|
65
|
+
const typeSymbol = findTypeSymbolAtPosition(document, params.position, graph, mod);
|
|
66
|
+
if (!typeSymbol) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
contents: {
|
|
71
|
+
kind: "markdown",
|
|
72
|
+
value: `\`\`\`rank\n${typeSymbol.name}: ${formatTypeNode(typeSymbol.type)}\n\`\`\``,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createConnection, ProposedFeatures, TextDocumentSyncKind, TextDocuments, } from "vscode-languageserver/node.js";
|
|
4
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { ProjectSession } from "./session.js";
|
|
7
|
+
import { publishDiagnostics } from "./features/diagnostics.js";
|
|
8
|
+
import { handleHover } from "./features/hover.js";
|
|
9
|
+
import { handleDefinition } from "./features/definition.js";
|
|
10
|
+
import { handleCompletion } from "./features/completion.js";
|
|
11
|
+
const connection = createConnection(ProposedFeatures.all);
|
|
12
|
+
const documents = new TextDocuments(TextDocument);
|
|
13
|
+
const session = new ProjectSession();
|
|
14
|
+
connection.onInitialize(() => ({
|
|
15
|
+
capabilities: {
|
|
16
|
+
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
17
|
+
hoverProvider: true,
|
|
18
|
+
definitionProvider: true,
|
|
19
|
+
completionProvider: {
|
|
20
|
+
triggerCharacters: [".", ":", " "],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
documents.onDidOpen(({ document }) => {
|
|
25
|
+
const filePath = fileURLToPath(document.uri);
|
|
26
|
+
void publishDiagnostics(connection, session, filePath);
|
|
27
|
+
});
|
|
28
|
+
documents.onDidChangeContent(({ document }) => {
|
|
29
|
+
const filePath = fileURLToPath(document.uri);
|
|
30
|
+
session.invalidate(filePath);
|
|
31
|
+
void publishDiagnostics(connection, session, filePath);
|
|
32
|
+
});
|
|
33
|
+
documents.onDidSave(({ document }) => {
|
|
34
|
+
const filePath = fileURLToPath(document.uri);
|
|
35
|
+
session.invalidate(filePath);
|
|
36
|
+
void publishDiagnostics(connection, session, filePath);
|
|
37
|
+
});
|
|
38
|
+
connection.onDidChangeWatchedFiles(({ changes }) => {
|
|
39
|
+
const manifestChanges = changes
|
|
40
|
+
.map(({ uri }) => fileURLToPath(uri))
|
|
41
|
+
.filter((filePath) => path.basename(filePath) === "rank.toml");
|
|
42
|
+
if (manifestChanges.length === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const manifestPath of manifestChanges) {
|
|
46
|
+
session.invalidate(manifestPath);
|
|
47
|
+
}
|
|
48
|
+
for (const document of documents.all()) {
|
|
49
|
+
const filePath = fileURLToPath(document.uri);
|
|
50
|
+
void publishDiagnostics(connection, session, filePath);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
connection.onHover((params) => handleHover(params, session, documents.get(params.textDocument.uri)));
|
|
54
|
+
connection.onDefinition((params) => handleDefinition(params, session, documents.get(params.textDocument.uri)));
|
|
55
|
+
connection.onCompletion((params) => handleCompletion(params, session, documents.get(params.textDocument.uri)));
|
|
56
|
+
documents.listen(connection);
|
|
57
|
+
connection.listen();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RootModuleGraph, type LoadedModule, type TypeCheckOptions } from "@rank-lang/compiler";
|
|
2
|
+
export declare class ProjectSession {
|
|
3
|
+
private readonly cache;
|
|
4
|
+
getModuleGraph(filePath: string): Promise<RootModuleGraph | null>;
|
|
5
|
+
getLoadedModule(graph: RootModuleGraph, filePath: string): LoadedModule | null;
|
|
6
|
+
getTypeCheckOptions(filePath: string, graph?: RootModuleGraph): Promise<TypeCheckOptions | undefined>;
|
|
7
|
+
invalidate(filePath: string): void;
|
|
8
|
+
private loadGraphForFile;
|
|
9
|
+
private cacheKeyForFile;
|
|
10
|
+
private preferredEntryPathForFile;
|
|
11
|
+
private nearestTestManifestPathForFile;
|
|
12
|
+
private testManifestUsesProviderStubs;
|
|
13
|
+
private findProjectRoot;
|
|
14
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { loadNearestRankManifestIfValid, loadProjectModuleGraphAsync, resolveSecurityOptions, } from "@rank-lang/compiler";
|
|
4
|
+
// Manages a per-project-root module graph cache. The cache is invalidated on
|
|
5
|
+
// textDocument/didChange and textDocument/didSave for any file in the project.
|
|
6
|
+
export class ProjectSession {
|
|
7
|
+
cache = new Map();
|
|
8
|
+
async getModuleGraph(filePath) {
|
|
9
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
10
|
+
const cacheKey = this.cacheKeyForFile(resolvedFilePath);
|
|
11
|
+
const cached = this.cache.get(cacheKey);
|
|
12
|
+
if (cached && !cached.invalidated && this.getLoadedModule(cached.graph, resolvedFilePath)) {
|
|
13
|
+
return cached.graph;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const graph = await this.loadGraphForFile(resolvedFilePath);
|
|
17
|
+
this.cache.set(cacheKey, { graph, invalidated: false });
|
|
18
|
+
return graph;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
getLoadedModule(graph, filePath) {
|
|
25
|
+
const resolved = path.resolve(filePath);
|
|
26
|
+
for (const mod of Object.values(graph.modules)) {
|
|
27
|
+
if (path.resolve(mod.filePath) === resolved) {
|
|
28
|
+
return mod;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
async getTypeCheckOptions(filePath, graph) {
|
|
34
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
35
|
+
const securityOptions = resolveSecurityOptions(loadNearestRankManifestIfValid(resolvedFilePath)?.security);
|
|
36
|
+
const testManifestPath = this.nearestTestManifestPathForFile(resolvedFilePath);
|
|
37
|
+
const providerCapabilities = new Set(securityOptions.compileTime.providerCapabilities);
|
|
38
|
+
if (testManifestPath && this.testManifestUsesProviderStubs(testManifestPath)) {
|
|
39
|
+
const loadedGraph = graph ?? await this.getModuleGraph(resolvedFilePath);
|
|
40
|
+
if (loadedGraph) {
|
|
41
|
+
for (const providerExport of Object.values(loadedGraph.providerExports)) {
|
|
42
|
+
if (providerExport.kind !== "backend") {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const capability of providerExport.capabilities) {
|
|
46
|
+
providerCapabilities.add(capability);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (providerCapabilities.size === 0) {
|
|
52
|
+
if (securityOptions.compileTime.providerMutationExports.length > 0 || securityOptions.compileTime.allowedHttpHosts) {
|
|
53
|
+
return {
|
|
54
|
+
...(securityOptions.compileTime.providerMutationExports.length > 0
|
|
55
|
+
? { providerMutationExports: securityOptions.compileTime.providerMutationExports }
|
|
56
|
+
: {}),
|
|
57
|
+
...(securityOptions.compileTime.allowedHttpHosts ? { allowedHttpHosts: securityOptions.compileTime.allowedHttpHosts } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
providerCapabilities: [...providerCapabilities].sort((left, right) => left.localeCompare(right)),
|
|
64
|
+
...(securityOptions.compileTime.providerMutationExports.length > 0
|
|
65
|
+
? { providerMutationExports: securityOptions.compileTime.providerMutationExports }
|
|
66
|
+
: {}),
|
|
67
|
+
...(securityOptions.compileTime.allowedHttpHosts ? { allowedHttpHosts: securityOptions.compileTime.allowedHttpHosts } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
invalidate(filePath) {
|
|
71
|
+
const cacheKey = this.cacheKeyForFile(filePath);
|
|
72
|
+
const cached = this.cache.get(cacheKey);
|
|
73
|
+
if (cached)
|
|
74
|
+
cached.invalidated = true;
|
|
75
|
+
}
|
|
76
|
+
async loadGraphForFile(filePath) {
|
|
77
|
+
const preferredEntryPath = this.preferredEntryPathForFile(filePath);
|
|
78
|
+
const securityOptions = resolveSecurityOptions(loadNearestRankManifestIfValid(filePath)?.security);
|
|
79
|
+
const graphOptions = securityOptions.compileTime.offline
|
|
80
|
+
? { offline: true }
|
|
81
|
+
: undefined;
|
|
82
|
+
if (preferredEntryPath && preferredEntryPath !== filePath) {
|
|
83
|
+
const graph = await loadProjectModuleGraphAsync(preferredEntryPath, graphOptions);
|
|
84
|
+
if (this.getLoadedModule(graph, filePath)) {
|
|
85
|
+
return graph;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return loadProjectModuleGraphAsync(filePath, graphOptions);
|
|
89
|
+
}
|
|
90
|
+
cacheKeyForFile(filePath) {
|
|
91
|
+
return this.findProjectRoot(filePath) ?? path.resolve(filePath);
|
|
92
|
+
}
|
|
93
|
+
preferredEntryPathForFile(filePath) {
|
|
94
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
95
|
+
if (path.basename(resolvedFilePath) === "main.rank") {
|
|
96
|
+
return resolvedFilePath;
|
|
97
|
+
}
|
|
98
|
+
let dir = path.dirname(resolvedFilePath);
|
|
99
|
+
const projectRoot = this.findProjectRoot(resolvedFilePath);
|
|
100
|
+
const stopDir = projectRoot ? path.resolve(projectRoot) : null;
|
|
101
|
+
for (let i = 0; i < 20; i++) {
|
|
102
|
+
const candidatePath = path.join(dir, "main.rank");
|
|
103
|
+
if (fs.existsSync(candidatePath)) {
|
|
104
|
+
return candidatePath;
|
|
105
|
+
}
|
|
106
|
+
if (stopDir && dir === stopDir) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
const parent = path.dirname(dir);
|
|
110
|
+
if (parent === dir) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
dir = parent;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
nearestTestManifestPathForFile(filePath) {
|
|
118
|
+
let dir = path.dirname(path.resolve(filePath));
|
|
119
|
+
const projectRoot = this.findProjectRoot(filePath);
|
|
120
|
+
const stopDir = projectRoot ? path.resolve(projectRoot) : null;
|
|
121
|
+
for (let i = 0; i < 20; i++) {
|
|
122
|
+
const candidatePath = path.join(dir, "rank-test.json");
|
|
123
|
+
if (fs.existsSync(candidatePath)) {
|
|
124
|
+
return candidatePath;
|
|
125
|
+
}
|
|
126
|
+
if (stopDir && dir === stopDir) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
const parent = path.dirname(dir);
|
|
130
|
+
if (parent === dir) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
dir = parent;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
testManifestUsesProviderStubs(manifestPath) {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
140
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const manifest = parsed;
|
|
144
|
+
const providerStubs = manifest.providerStubs;
|
|
145
|
+
if (manifest.kind !== "rank/test-case" || manifest.version !== 1 || typeof providerStubs !== "string" || providerStubs.length === 0) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return fs.existsSync(path.resolve(path.dirname(manifestPath), providerStubs));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
findProjectRoot(filePath) {
|
|
155
|
+
let dir = path.dirname(path.resolve(filePath));
|
|
156
|
+
for (let i = 0; i < 20; i++) {
|
|
157
|
+
if (fs.existsSync(path.join(dir, "rank.toml"))) {
|
|
158
|
+
return dir;
|
|
159
|
+
}
|
|
160
|
+
const parent = path.dirname(dir);
|
|
161
|
+
if (parent === dir)
|
|
162
|
+
break;
|
|
163
|
+
dir = parent;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SourcePosition, SourceSpan, Diagnostic as CompilerDiagnostic } from "@rank-lang/compiler";
|
|
2
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
|
+
import { type Position as LspPosition, type Range as LspRange, type Diagnostic as LspDiagnostic } from "vscode-languageserver/node.js";
|
|
4
|
+
export declare function toSourcePosition(pos: LspPosition, document?: TextDocument): SourcePosition;
|
|
5
|
+
export declare function toLspRange(span: SourceSpan): LspRange;
|
|
6
|
+
export declare function toLspDiagnostic(d: CompilerDiagnostic): LspDiagnostic;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { DiagnosticSeverity, } from "vscode-languageserver/node.js";
|
|
3
|
+
// Compiler uses 1-indexed lines/columns; LSP uses 0-indexed.
|
|
4
|
+
export function toSourcePosition(pos, document) {
|
|
5
|
+
return {
|
|
6
|
+
offset: document?.offsetAt(pos) ?? 0,
|
|
7
|
+
line: pos.line + 1,
|
|
8
|
+
column: pos.character + 1,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function toLspRange(span) {
|
|
12
|
+
return {
|
|
13
|
+
start: { line: span.start.line - 1, character: span.start.column - 1 },
|
|
14
|
+
end: { line: span.end.line - 1, character: span.end.column - 1 },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function toLspDiagnostic(d) {
|
|
18
|
+
const range = d.span
|
|
19
|
+
? toLspRange(d.span)
|
|
20
|
+
: { start: { line: 0, character: 0 }, end: { line: 0, character: Number.MAX_SAFE_INTEGER } };
|
|
21
|
+
const diagnostic = {
|
|
22
|
+
range,
|
|
23
|
+
severity: d.severity === "error" ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning,
|
|
24
|
+
message: d.message,
|
|
25
|
+
source: "rank",
|
|
26
|
+
code: d.code,
|
|
27
|
+
...(d.labels && d.labels.length > 0
|
|
28
|
+
? {
|
|
29
|
+
relatedInformation: d.labels.map((label) => ({
|
|
30
|
+
location: {
|
|
31
|
+
uri: pathToFileURL(label.sourcePath).href,
|
|
32
|
+
range: toLspRange(label.span),
|
|
33
|
+
},
|
|
34
|
+
message: label.message,
|
|
35
|
+
})),
|
|
36
|
+
}
|
|
37
|
+
: {}),
|
|
38
|
+
};
|
|
39
|
+
return diagnostic;
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type LoadedModule } from "@rank-lang/compiler";
|
|
2
|
+
import type { Position as LspPosition } from "vscode-languageserver/node.js";
|
|
3
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
4
|
+
export declare function findStdlibHoverAtPosition(document: TextDocument, position: LspPosition, loadedModule: LoadedModule): string | null;
|