@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { formatStdlibHoverMarkdown, lookupStdlibBuiltinHoverInfo, lookupStdlibBuiltinValue, lookupStdlibBuiltinValueByQualifiedName, lookupStdlibModuleHoverInfo, lookupStdlibModuleValue, lookupStdlibTypeExportHoverInfo, stdModuleNamespacePath, } from "@rank-lang/compiler";
|
|
2
|
+
import { identifierAtPosition } from "./type-symbols.js";
|
|
3
|
+
function importedStdNamespaceModulePath(loadedModule, name) {
|
|
4
|
+
for (const importNode of loadedModule.program.imports) {
|
|
5
|
+
if (importNode.modulePath !== "std" || importNode.clause.kind !== "NamedImportClause") {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
if (!importNode.clause.names.includes(name)) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
return stdModuleNamespacePath(name) ?? null;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function importedStdMemberModulePath(loadedModule, name) {
|
|
16
|
+
for (const importNode of loadedModule.program.imports) {
|
|
17
|
+
if (!importNode.modulePath.startsWith("std::") || importNode.clause.kind !== "NamedImportClause") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (!importNode.clause.names.includes(name)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
return importNode.modulePath;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function directStdlibHover(identifier) {
|
|
28
|
+
const builtinId = lookupStdlibBuiltinValueByQualifiedName(identifier);
|
|
29
|
+
if (builtinId) {
|
|
30
|
+
const hover = lookupStdlibBuiltinHoverInfo(builtinId);
|
|
31
|
+
return hover ? formatStdlibHoverMarkdown(hover) : null;
|
|
32
|
+
}
|
|
33
|
+
const segments = identifier.split("::");
|
|
34
|
+
if (segments.length >= 3) {
|
|
35
|
+
const exportName = segments[segments.length - 1];
|
|
36
|
+
const modulePath = segments.slice(0, -1).join("::");
|
|
37
|
+
if (exportName) {
|
|
38
|
+
const typeHover = lookupStdlibTypeExportHoverInfo(modulePath, exportName);
|
|
39
|
+
if (typeHover) {
|
|
40
|
+
return formatStdlibHoverMarkdown(typeHover);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const moduleValue = lookupStdlibModuleValue(identifier);
|
|
45
|
+
if (moduleValue) {
|
|
46
|
+
const hover = lookupStdlibBuiltinHoverInfo(moduleValue);
|
|
47
|
+
return hover ? formatStdlibHoverMarkdown(hover) : null;
|
|
48
|
+
}
|
|
49
|
+
const moduleHover = lookupStdlibModuleHoverInfo(identifier);
|
|
50
|
+
return moduleHover ? formatStdlibHoverMarkdown(moduleHover) : null;
|
|
51
|
+
}
|
|
52
|
+
function importedQualifiedStdlibHover(loadedModule, identifier) {
|
|
53
|
+
const segments = identifier.split("::");
|
|
54
|
+
if (segments.length !== 2) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const qualifier = segments[0];
|
|
58
|
+
const exportName = segments[1];
|
|
59
|
+
if (!qualifier || !exportName) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const modulePath = importedStdNamespaceModulePath(loadedModule, qualifier);
|
|
63
|
+
if (!modulePath) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const builtinId = lookupStdlibBuiltinValue(modulePath, exportName);
|
|
67
|
+
if (builtinId) {
|
|
68
|
+
const hover = lookupStdlibBuiltinHoverInfo(builtinId);
|
|
69
|
+
return hover ? formatStdlibHoverMarkdown(hover) : null;
|
|
70
|
+
}
|
|
71
|
+
const typeHover = lookupStdlibTypeExportHoverInfo(modulePath, exportName);
|
|
72
|
+
return typeHover ? formatStdlibHoverMarkdown(typeHover) : null;
|
|
73
|
+
}
|
|
74
|
+
function importedBareStdlibHover(loadedModule, identifier) {
|
|
75
|
+
const namespaceModulePath = importedStdNamespaceModulePath(loadedModule, identifier);
|
|
76
|
+
if (namespaceModulePath) {
|
|
77
|
+
const moduleValue = lookupStdlibModuleValue(namespaceModulePath);
|
|
78
|
+
if (moduleValue) {
|
|
79
|
+
const hover = lookupStdlibBuiltinHoverInfo(moduleValue);
|
|
80
|
+
return hover ? formatStdlibHoverMarkdown(hover) : null;
|
|
81
|
+
}
|
|
82
|
+
const moduleHover = lookupStdlibModuleHoverInfo(namespaceModulePath);
|
|
83
|
+
return moduleHover ? formatStdlibHoverMarkdown(moduleHover) : null;
|
|
84
|
+
}
|
|
85
|
+
const memberModulePath = importedStdMemberModulePath(loadedModule, identifier);
|
|
86
|
+
if (!memberModulePath) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const builtinId = lookupStdlibBuiltinValue(memberModulePath, identifier);
|
|
90
|
+
if (builtinId) {
|
|
91
|
+
const hover = lookupStdlibBuiltinHoverInfo(builtinId);
|
|
92
|
+
return hover ? formatStdlibHoverMarkdown(hover) : null;
|
|
93
|
+
}
|
|
94
|
+
const typeHover = lookupStdlibTypeExportHoverInfo(memberModulePath, identifier);
|
|
95
|
+
return typeHover ? formatStdlibHoverMarkdown(typeHover) : null;
|
|
96
|
+
}
|
|
97
|
+
export function findStdlibHoverAtPosition(document, position, loadedModule) {
|
|
98
|
+
const identifier = identifierAtPosition(document, position);
|
|
99
|
+
if (!identifier) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (identifier.startsWith("std::")) {
|
|
103
|
+
return directStdlibHover(identifier);
|
|
104
|
+
}
|
|
105
|
+
if (identifier.includes("::")) {
|
|
106
|
+
return importedQualifiedStdlibHover(loadedModule, identifier);
|
|
107
|
+
}
|
|
108
|
+
return importedBareStdlibHover(loadedModule, identifier);
|
|
109
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LoadedModule, RootModuleGraph, SourceSpan, TypeNode } from "@rank-lang/compiler";
|
|
2
|
+
import type { Position as LspPosition } from "vscode-languageserver/node.js";
|
|
3
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
4
|
+
interface TypeSymbolTarget {
|
|
5
|
+
filePath: string;
|
|
6
|
+
name: string;
|
|
7
|
+
span: SourceSpan;
|
|
8
|
+
type: TypeNode;
|
|
9
|
+
}
|
|
10
|
+
export declare function identifierAtPosition(document: TextDocument, position: LspPosition): string | null;
|
|
11
|
+
export declare function findTypeSymbolAtPosition(document: TextDocument, position: LspPosition, moduleGraph: RootModuleGraph, loadedModule: LoadedModule): TypeSymbolTarget | null;
|
|
12
|
+
export declare function formatTypeNode(typeNode: TypeNode): string;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
function isIdentifierCharacter(char) {
|
|
2
|
+
return char !== undefined && /[A-Za-z0-9_:]/.test(char);
|
|
3
|
+
}
|
|
4
|
+
export function identifierAtPosition(document, position) {
|
|
5
|
+
const text = document.getText();
|
|
6
|
+
const offset = document.offsetAt(position);
|
|
7
|
+
let start = offset;
|
|
8
|
+
let end = offset;
|
|
9
|
+
if (isIdentifierCharacter(text[offset])) {
|
|
10
|
+
end += 1;
|
|
11
|
+
}
|
|
12
|
+
else if (isIdentifierCharacter(text[offset - 1])) {
|
|
13
|
+
start -= 1;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
while (start > 0 && isIdentifierCharacter(text[start - 1])) {
|
|
19
|
+
start -= 1;
|
|
20
|
+
}
|
|
21
|
+
while (end < text.length && isIdentifierCharacter(text[end])) {
|
|
22
|
+
end += 1;
|
|
23
|
+
}
|
|
24
|
+
return text.slice(start, end);
|
|
25
|
+
}
|
|
26
|
+
function typeAliasDeclarationInModule(loadedModule, name) {
|
|
27
|
+
return loadedModule.program.declarations.find((declaration) => declaration.kind === "TypeAliasDeclaration" && declaration.name === name);
|
|
28
|
+
}
|
|
29
|
+
function localTypeSymbolTarget(loadedModule, name) {
|
|
30
|
+
const declaration = typeAliasDeclarationInModule(loadedModule, name);
|
|
31
|
+
if (!declaration?.nameSpan) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
filePath: loadedModule.filePath,
|
|
36
|
+
name,
|
|
37
|
+
span: declaration.nameSpan,
|
|
38
|
+
type: declaration.type,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function findTypeSymbolAtPosition(document, position, moduleGraph, loadedModule) {
|
|
42
|
+
const name = identifierAtPosition(document, position);
|
|
43
|
+
if (!name || name.includes("::")) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const local = localTypeSymbolTarget(loadedModule, name);
|
|
47
|
+
if (local) {
|
|
48
|
+
return local;
|
|
49
|
+
}
|
|
50
|
+
for (const importNode of loadedModule.program.imports) {
|
|
51
|
+
if (importNode.clause.kind !== "NamedImportClause") {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!importNode.clause.names.includes(name)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
for (const mod of Object.values(moduleGraph.modules)) {
|
|
58
|
+
if (mod === loadedModule || !mod.exportedTypeNames.includes(name)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const imported = localTypeSymbolTarget(mod, name);
|
|
62
|
+
if (imported) {
|
|
63
|
+
return imported;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
export function formatTypeNode(typeNode) {
|
|
70
|
+
switch (typeNode.kind) {
|
|
71
|
+
case "NamedType":
|
|
72
|
+
return typeNode.name;
|
|
73
|
+
case "NeverType":
|
|
74
|
+
return "never";
|
|
75
|
+
case "ListType":
|
|
76
|
+
return `[${formatTypeNode(typeNode.itemType)}]`;
|
|
77
|
+
case "LiteralType":
|
|
78
|
+
return typeNode.lexeme;
|
|
79
|
+
case "ParseTemplateType":
|
|
80
|
+
return `parse${typeNode.raw}`;
|
|
81
|
+
case "RoutePathType":
|
|
82
|
+
return `match\`${typeNode.raw}\``;
|
|
83
|
+
case "GenericType":
|
|
84
|
+
return `${typeNode.name}<${typeNode.arguments.map((argument) => formatTypeNode(argument)).join(", ")}>`;
|
|
85
|
+
case "ConfiguredType":
|
|
86
|
+
return `${formatTypeNode(typeNode.base)} { ... }`;
|
|
87
|
+
case "MappedType": {
|
|
88
|
+
const optionalSuffix = typeNode.optional === "add" ? "?" : typeNode.optional === "remove" ? "-?" : "";
|
|
89
|
+
return `Object { [${typeNode.keyName} in ${formatTypeNode(typeNode.domainType)}]${optionalSuffix}: ${formatTypeNode(typeNode.valueType)} }`;
|
|
90
|
+
}
|
|
91
|
+
case "IndexedAccessType":
|
|
92
|
+
return `${formatTypeNode(typeNode.objectType)}[${formatTypeNode(typeNode.indexType)}]`;
|
|
93
|
+
case "KeyofType":
|
|
94
|
+
return `keyof ${formatTypeNode(typeNode.operand)}`;
|
|
95
|
+
case "ConditionalType":
|
|
96
|
+
return `${formatTypeNode(typeNode.checkType)} extends ${formatTypeNode(typeNode.extendsType)} ? ${formatTypeNode(typeNode.trueType)} : ${formatTypeNode(typeNode.falseType)}`;
|
|
97
|
+
case "ObjectType": {
|
|
98
|
+
const fields = typeNode.fields.map((field) => `${field.name}${field.optional ? "?" : ""}: ${formatTypeNode(field.valueType)}`);
|
|
99
|
+
if (typeNode.restField) {
|
|
100
|
+
fields.push(`...: ${formatTypeNode(typeNode.restField.valueType)}`);
|
|
101
|
+
}
|
|
102
|
+
return `Object { ${fields.join(", ")} }`;
|
|
103
|
+
}
|
|
104
|
+
case "RefinementType": {
|
|
105
|
+
const fields = typeNode.fields.map((field) => `${field.name}${field.optional ? "?" : ""}: ${formatTypeNode(field.valueType)}`);
|
|
106
|
+
if (typeNode.restField) {
|
|
107
|
+
fields.push(`...: ${formatTypeNode(typeNode.restField.valueType)}`);
|
|
108
|
+
}
|
|
109
|
+
return `${formatTypeNode(typeNode.base)} { ${fields.join(", ")} }`;
|
|
110
|
+
}
|
|
111
|
+
case "FunctionType":
|
|
112
|
+
return `(${typeNode.parameterTypes.map((parameterType) => formatTypeNode(parameterType)).join(", ")}) => ${formatTypeNode(typeNode.returnType)}`;
|
|
113
|
+
case "UnionType":
|
|
114
|
+
return typeNode.members.map((member) => formatTypeNode(member)).join(" | ");
|
|
115
|
+
case "IntersectionType":
|
|
116
|
+
return typeNode.members.map((member) => formatTypeNode(member)).join(" & ");
|
|
117
|
+
}
|
|
118
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rank-lang/lsp",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Rank Language Server Protocol server",
|
|
5
|
+
"author": "Benjamin Goodman <b@ben.website>",
|
|
6
|
+
"homepage": "https://rank-lang.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://gitlab.com/ben_goodman/rank.git",
|
|
10
|
+
"directory": "packages/lsp"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://gitlab.com/ben_goodman/rank/-/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./src/server.ts",
|
|
19
|
+
"development": "./src/server.ts",
|
|
20
|
+
"default": "./dist/server.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"rank-lsp": "dist/server.js"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
30
|
+
"test": "NODE_OPTIONS='--conditions=development' vitest run --passWithNoTests"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@rank-lang/compiler": "0.3.0",
|
|
37
|
+
"@rank-lang/server-runtime": "0.3.0",
|
|
38
|
+
"vscode-languageserver": "^9.0.1",
|
|
39
|
+
"vscode-languageserver-textdocument": "^1.0.12"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { completionItemsAtPosition } from "@rank-lang/compiler";
|
|
2
|
+
import type {
|
|
3
|
+
CompletionItem as LspCompletionItem,
|
|
4
|
+
TextDocumentPositionParams,
|
|
5
|
+
} from "vscode-languageserver/node.js";
|
|
6
|
+
import { CompletionItemKind } from "vscode-languageserver/node.js";
|
|
7
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import type { ProjectSession } from "../session.js";
|
|
10
|
+
import { toSourcePosition } from "../util/convert.js";
|
|
11
|
+
|
|
12
|
+
export async function handleCompletion(
|
|
13
|
+
params: TextDocumentPositionParams,
|
|
14
|
+
session: ProjectSession,
|
|
15
|
+
document?: TextDocument,
|
|
16
|
+
): Promise<LspCompletionItem[]> {
|
|
17
|
+
const filePath = fileURLToPath(params.textDocument.uri);
|
|
18
|
+
const graph = await session.getModuleGraph(filePath);
|
|
19
|
+
|
|
20
|
+
if (!graph) return [];
|
|
21
|
+
|
|
22
|
+
const mod = session.getLoadedModule(graph, filePath);
|
|
23
|
+
if (!mod) return [];
|
|
24
|
+
|
|
25
|
+
const position = toSourcePosition(params.position, document);
|
|
26
|
+
const items = completionItemsAtPosition(graph, mod, position);
|
|
27
|
+
|
|
28
|
+
return items.map((item) => ({
|
|
29
|
+
label: item.label,
|
|
30
|
+
kind:
|
|
31
|
+
item.kind === "type"
|
|
32
|
+
? CompletionItemKind.Class
|
|
33
|
+
: item.kind === "keyword"
|
|
34
|
+
? CompletionItemKind.Keyword
|
|
35
|
+
: CompletionItemKind.Variable,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { findDefinitionAtPosition } from "@rank-lang/compiler";
|
|
2
|
+
import type {
|
|
3
|
+
TextDocumentPositionParams,
|
|
4
|
+
Location,
|
|
5
|
+
} from "vscode-languageserver/node.js";
|
|
6
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import type { ProjectSession } from "../session.js";
|
|
9
|
+
import { toSourcePosition, toLspRange } from "../util/convert.js";
|
|
10
|
+
import { findTypeSymbolAtPosition } from "../util/type-symbols.js";
|
|
11
|
+
|
|
12
|
+
export async function handleDefinition(
|
|
13
|
+
params: TextDocumentPositionParams,
|
|
14
|
+
session: ProjectSession,
|
|
15
|
+
document?: TextDocument,
|
|
16
|
+
): Promise<Location | null> {
|
|
17
|
+
const filePath = fileURLToPath(params.textDocument.uri);
|
|
18
|
+
const graph = await session.getModuleGraph(filePath);
|
|
19
|
+
|
|
20
|
+
if (!graph) return null;
|
|
21
|
+
|
|
22
|
+
const mod = session.getLoadedModule(graph, filePath);
|
|
23
|
+
if (!mod) return null;
|
|
24
|
+
|
|
25
|
+
const position = toSourcePosition(params.position, document);
|
|
26
|
+
const definition = findDefinitionAtPosition(graph, mod, position);
|
|
27
|
+
|
|
28
|
+
if (definition) {
|
|
29
|
+
return {
|
|
30
|
+
uri: pathToFileURL(definition.filePath).href,
|
|
31
|
+
range: toLspRange(definition.span),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!document) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const typeSymbol = findTypeSymbolAtPosition(document, params.position, graph, mod);
|
|
40
|
+
|
|
41
|
+
if (!typeSymbol) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
uri: pathToFileURL(typeSymbol.filePath).href,
|
|
47
|
+
range: toLspRange(typeSymbol.span),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveModuleImports,
|
|
3
|
+
resolveModuleReferences,
|
|
4
|
+
resolveModuleTypeNames,
|
|
5
|
+
checkModuleTypes,
|
|
6
|
+
type Diagnostic as CompilerDiagnostic,
|
|
7
|
+
type DeclarationNode,
|
|
8
|
+
type LoadedModule,
|
|
9
|
+
type TypeNode,
|
|
10
|
+
type ValueBindingDeclarationNode,
|
|
11
|
+
} from "@rank-lang/compiler";
|
|
12
|
+
import { collectRankServerAppSurfaceDiagnostics } from "@rank-lang/server-runtime";
|
|
13
|
+
import type { Connection } from "vscode-languageserver/node.js";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
|
+
import type { ProjectSession } from "../session.js";
|
|
16
|
+
import { toLspDiagnostic } from "../util/convert.js";
|
|
17
|
+
|
|
18
|
+
interface DiagnosticSuppression {
|
|
19
|
+
startOffset: number;
|
|
20
|
+
endOffset: number;
|
|
21
|
+
codes: Set<string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseIgnoredDiagnosticCodes(documentation: readonly string[]): Set<string> {
|
|
25
|
+
const codes = new Set<string>();
|
|
26
|
+
|
|
27
|
+
for (const line of documentation) {
|
|
28
|
+
const match = /^rank-ignore\b(.*)$/u.exec(line.trim());
|
|
29
|
+
|
|
30
|
+
if (!match) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const code of (match[1] ?? "").split(/[\s,]+/u).map((value) => value.trim()).filter((value) => value.length > 0)) {
|
|
35
|
+
codes.add(code);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return codes;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function declarationStartOffset(declaration: DeclarationNode): number | undefined {
|
|
43
|
+
switch (declaration.kind) {
|
|
44
|
+
case "ValueBindingDeclaration":
|
|
45
|
+
case "TypeAliasDeclaration":
|
|
46
|
+
return declaration.nameSpan?.start.offset;
|
|
47
|
+
case "DestructuringBindingDeclaration":
|
|
48
|
+
return declaration.fieldSpans
|
|
49
|
+
? Math.min(...Object.values(declaration.fieldSpans).map((span) => span.start.offset))
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectDiagnosticSuppressions(loadedModule: LoadedModule): DiagnosticSuppression[] {
|
|
55
|
+
const declarationsWithStarts = loadedModule.program.declarations.map((declaration) => ({
|
|
56
|
+
declaration,
|
|
57
|
+
startOffset: declarationStartOffset(declaration),
|
|
58
|
+
}));
|
|
59
|
+
const suppressions: DiagnosticSuppression[] = [];
|
|
60
|
+
|
|
61
|
+
for (let index = 0; index < declarationsWithStarts.length; index += 1) {
|
|
62
|
+
const current = declarationsWithStarts[index];
|
|
63
|
+
|
|
64
|
+
if (!current) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const startOffset = current.startOffset;
|
|
69
|
+
|
|
70
|
+
if (startOffset === undefined) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const codes = parseIgnoredDiagnosticCodes(current.declaration.documentation);
|
|
75
|
+
|
|
76
|
+
if (codes.size === 0) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const nextStartOffset = declarationsWithStarts
|
|
81
|
+
.slice(index + 1)
|
|
82
|
+
.map((candidate) => candidate.startOffset)
|
|
83
|
+
.find((candidate): candidate is number => candidate !== undefined);
|
|
84
|
+
|
|
85
|
+
suppressions.push({
|
|
86
|
+
startOffset,
|
|
87
|
+
endOffset: nextStartOffset ?? Number.POSITIVE_INFINITY,
|
|
88
|
+
codes,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return suppressions;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isSuppressedDiagnostic(diagnostic: CompilerDiagnostic, suppressions: readonly DiagnosticSuppression[]): boolean {
|
|
96
|
+
if (!diagnostic.span) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return suppressions.some((suppression) => (
|
|
101
|
+
diagnostic.span!.start.offset >= suppression.startOffset
|
|
102
|
+
&& diagnostic.span!.start.offset < suppression.endOffset
|
|
103
|
+
&& suppression.codes.has(diagnostic.code)
|
|
104
|
+
));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function exportedValueDeclaration(loadedModule: LoadedModule, name: string): ValueBindingDeclarationNode | undefined {
|
|
108
|
+
return loadedModule.program.declarations.find(
|
|
109
|
+
(declaration): declaration is ValueBindingDeclarationNode =>
|
|
110
|
+
declaration.kind === "ValueBindingDeclaration"
|
|
111
|
+
&& declaration.visibility === "pub"
|
|
112
|
+
&& declaration.name === name,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isExecutionContextTypeName(name: string): boolean {
|
|
117
|
+
return name === "ExecutionContext"
|
|
118
|
+
|| name === "Runtime::ExecutionContext"
|
|
119
|
+
|| name === "std::Runtime::ExecutionContext";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isServeExecutionContextTypeAnnotation(typeNode: TypeNode | undefined): boolean {
|
|
123
|
+
return !!typeNode
|
|
124
|
+
&& typeNode.kind === "GenericType"
|
|
125
|
+
&& typeNode.arguments.length === 1
|
|
126
|
+
&& isExecutionContextTypeName(typeNode.name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function shouldPublishServeDiagnostics(loadedModule: LoadedModule): boolean {
|
|
130
|
+
if (exportedValueDeclaration(loadedModule, "config")) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const mainDeclaration = exportedValueDeclaration(loadedModule, "main");
|
|
135
|
+
|
|
136
|
+
return !!mainDeclaration
|
|
137
|
+
&& mainDeclaration.value.kind === "FunctionExpression"
|
|
138
|
+
&& (
|
|
139
|
+
mainDeclaration.value.parameters.length === 0
|
|
140
|
+
|| (
|
|
141
|
+
mainDeclaration.value.parameters.length === 1
|
|
142
|
+
&& isServeExecutionContextTypeAnnotation(mainDeclaration.value.parameters[0]?.typeAnnotation)
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function publishDiagnostics(
|
|
148
|
+
connection: Connection,
|
|
149
|
+
session: ProjectSession,
|
|
150
|
+
filePath: string,
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
const graph = await session.getModuleGraph(filePath);
|
|
153
|
+
|
|
154
|
+
if (!graph) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const importDiagnostics = resolveModuleImports(graph);
|
|
159
|
+
const refDiagnostics = resolveModuleReferences(graph);
|
|
160
|
+
const typeDiagnostics = resolveModuleTypeNames(graph);
|
|
161
|
+
const checkDiagnostics = checkModuleTypes(graph, await session.getTypeCheckOptions(filePath, graph));
|
|
162
|
+
const entryModule = graph.modules[graph.entryModulePath];
|
|
163
|
+
const serveDiagnostics = entryModule && shouldPublishServeDiagnostics(entryModule)
|
|
164
|
+
? collectRankServerAppSurfaceDiagnostics(graph, entryModule.filePath)
|
|
165
|
+
: [];
|
|
166
|
+
|
|
167
|
+
const allDiagnostics = [
|
|
168
|
+
...graph.diagnostics,
|
|
169
|
+
...importDiagnostics,
|
|
170
|
+
...refDiagnostics,
|
|
171
|
+
...typeDiagnostics,
|
|
172
|
+
...checkDiagnostics,
|
|
173
|
+
...serveDiagnostics,
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
// Group diagnostics by source file and publish per-file.
|
|
177
|
+
const byFile = new Map<string, typeof allDiagnostics>();
|
|
178
|
+
|
|
179
|
+
for (const d of allDiagnostics) {
|
|
180
|
+
let list = byFile.get(d.sourcePath);
|
|
181
|
+
if (!list) {
|
|
182
|
+
list = [];
|
|
183
|
+
byFile.set(d.sourcePath, list);
|
|
184
|
+
}
|
|
185
|
+
list.push(d);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Publish for the affected file and clear any files with no diagnostics.
|
|
189
|
+
for (const mod of Object.values(graph.modules)) {
|
|
190
|
+
const uri = pathToFileURL(mod.filePath).href;
|
|
191
|
+
const suppressions = collectDiagnosticSuppressions(mod);
|
|
192
|
+
const diags = (byFile.get(mod.filePath) ?? []).filter((diagnostic) => !isSuppressedDiagnostic(diagnostic, suppressions));
|
|
193
|
+
connection.sendDiagnostics({ uri, diagnostics: diags.map(toLspDiagnostic) });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hoverInfoAtPosition,
|
|
3
|
+
queryModuleBindingType,
|
|
4
|
+
type LoadedModule,
|
|
5
|
+
type RootModuleGraph,
|
|
6
|
+
type SourcePosition,
|
|
7
|
+
type ValueBindingDeclarationNode,
|
|
8
|
+
} from "@rank-lang/compiler";
|
|
9
|
+
import { collectRankServerAppSurfaceDiagnostics } from "@rank-lang/server-runtime";
|
|
10
|
+
import type {
|
|
11
|
+
TextDocumentPositionParams,
|
|
12
|
+
Hover,
|
|
13
|
+
} from "vscode-languageserver/node.js";
|
|
14
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import type { ProjectSession } from "../session.js";
|
|
17
|
+
import { toSourcePosition } from "../util/convert.js";
|
|
18
|
+
import { findStdlibHoverAtPosition } from "../util/stdlib-hover.js";
|
|
19
|
+
import { findTypeSymbolAtPosition, formatTypeNode } from "../util/type-symbols.js";
|
|
20
|
+
|
|
21
|
+
function containsPosition(
|
|
22
|
+
span: { start: SourcePosition; end: SourcePosition } | undefined,
|
|
23
|
+
position: SourcePosition,
|
|
24
|
+
): boolean {
|
|
25
|
+
return !!span && span.start.offset <= position.offset && position.offset < span.end.offset;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function exportedValueDeclaration(loadedModule: LoadedModule, name: string): ValueBindingDeclarationNode | undefined {
|
|
29
|
+
return loadedModule.program.declarations.find(
|
|
30
|
+
(declaration): declaration is ValueBindingDeclarationNode =>
|
|
31
|
+
declaration.kind === "ValueBindingDeclaration"
|
|
32
|
+
&& declaration.visibility === "pub"
|
|
33
|
+
&& declaration.name === name,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function serveConfigHoverAtPosition(
|
|
38
|
+
moduleGraph: RootModuleGraph,
|
|
39
|
+
loadedModule: LoadedModule,
|
|
40
|
+
position: SourcePosition,
|
|
41
|
+
): string | null {
|
|
42
|
+
const entryModule = moduleGraph.modules[moduleGraph.entryModulePath];
|
|
43
|
+
|
|
44
|
+
if (!entryModule || entryModule.filePath !== loadedModule.filePath) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const configDeclaration = exportedValueDeclaration(loadedModule, "config");
|
|
49
|
+
|
|
50
|
+
if (!configDeclaration || !containsPosition(configDeclaration.nameSpan, position)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const actualType = queryModuleBindingType(moduleGraph, loadedModule, "config", false);
|
|
55
|
+
|
|
56
|
+
if (!actualType) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const serveDiagnostics = collectRankServerAppSurfaceDiagnostics(moduleGraph, loadedModule.filePath);
|
|
61
|
+
const lines = [
|
|
62
|
+
"```rank",
|
|
63
|
+
`config: ${actualType}`,
|
|
64
|
+
"```",
|
|
65
|
+
"",
|
|
66
|
+
"Serve config expects `HTTP::AppConfig` or `() -> HTTP::AppConfig`.",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
if (serveDiagnostics.some((diagnostic) => diagnostic.code === "SRV004")) {
|
|
70
|
+
lines.push(
|
|
71
|
+
"",
|
|
72
|
+
"This binding is invalid for serve-time `pub config`: it must be an object literal or zero-argument function.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function handleHover(
|
|
80
|
+
params: TextDocumentPositionParams,
|
|
81
|
+
session: ProjectSession,
|
|
82
|
+
document?: TextDocument,
|
|
83
|
+
): Promise<Hover | null> {
|
|
84
|
+
const filePath = fileURLToPath(params.textDocument.uri);
|
|
85
|
+
const graph = await session.getModuleGraph(filePath);
|
|
86
|
+
|
|
87
|
+
if (!graph) return null;
|
|
88
|
+
|
|
89
|
+
const mod = session.getLoadedModule(graph, filePath);
|
|
90
|
+
if (!mod) return null;
|
|
91
|
+
|
|
92
|
+
const position = toSourcePosition(params.position, document);
|
|
93
|
+
const serveConfigMarkdown = serveConfigHoverAtPosition(graph, mod, position);
|
|
94
|
+
|
|
95
|
+
if (serveConfigMarkdown) {
|
|
96
|
+
return { contents: { kind: "markdown", value: serveConfigMarkdown } };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const markdown = hoverInfoAtPosition(graph, mod, position);
|
|
100
|
+
|
|
101
|
+
if (markdown) {
|
|
102
|
+
return { contents: { kind: "markdown", value: markdown } };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!document) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const stdlibMarkdown = findStdlibHoverAtPosition(document, params.position, mod);
|
|
110
|
+
|
|
111
|
+
if (stdlibMarkdown) {
|
|
112
|
+
return { contents: { kind: "markdown", value: stdlibMarkdown } };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const typeSymbol = findTypeSymbolAtPosition(document, params.position, graph, mod);
|
|
116
|
+
|
|
117
|
+
if (!typeSymbol) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
contents: {
|
|
123
|
+
kind: "markdown",
|
|
124
|
+
value: `\`\`\`rank\n${typeSymbol.name}: ${formatTypeNode(typeSymbol.type)}\n\`\`\``,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|