@venn-lang/lsp 0.1.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/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/bin/venn-lsp.d.mts +1 -0
- package/dist/bin/venn-lsp.mjs +17 -0
- package/dist/bin/venn-lsp.mjs.map +1 -0
- package/dist/index.d.mts +722 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/server-BET60xwK.mjs +5376 -0
- package/dist/server-BET60xwK.mjs.map +1 -0
- package/package.json +65 -0
- package/src/bin/venn-lsp.ts +17 -0
- package/src/catalog/build-catalog.ts +72 -0
- package/src/catalog/catalog.types.ts +44 -0
- package/src/catalog/index.ts +2 -0
- package/src/code-actions/anchor.ts +49 -0
- package/src/code-actions/code-actions.ts +139 -0
- package/src/code-actions/exporting-modules.ts +73 -0
- package/src/code-actions/index.ts +5 -0
- package/src/completion/argument-items.ts +81 -0
- package/src/completion/completion.ts +416 -0
- package/src/completion/completion.types.ts +38 -0
- package/src/completion/context.ts +144 -0
- package/src/completion/icons.ts +83 -0
- package/src/completion/index.ts +4 -0
- package/src/completion/items.ts +232 -0
- package/src/completion/member-items.ts +147 -0
- package/src/completion/module-paths.ts +63 -0
- package/src/completion/options-map-items.ts +47 -0
- package/src/completion/read-from.ts +77 -0
- package/src/completion/type-name-items.ts +62 -0
- package/src/deco/builtin-decos.ts +35 -0
- package/src/deco/builtin-docs.ts +26 -0
- package/src/deco/declared-deco.ts +56 -0
- package/src/deco/deco.types.ts +23 -0
- package/src/deco/decos-in-scope.ts +63 -0
- package/src/deco/imported-decos.ts +49 -0
- package/src/deco/index.ts +9 -0
- package/src/deco/kind-of.ts +15 -0
- package/src/deco/render-deco.ts +44 -0
- package/src/definition/definition.ts +93 -0
- package/src/definition/index.ts +1 -0
- package/src/docs/doc.types.ts +17 -0
- package/src/docs/index.ts +4 -0
- package/src/docs/parse-doc.ts +42 -0
- package/src/docs/read-doc.ts +45 -0
- package/src/docs/render-doc.ts +42 -0
- package/src/document/exported-names.ts +30 -0
- package/src/document/expression-path.ts +43 -0
- package/src/document/find-binding.ts +82 -0
- package/src/document/host-at.ts +61 -0
- package/src/document/imported-modules.ts +91 -0
- package/src/document/imported-names.ts +15 -0
- package/src/document/index.ts +19 -0
- package/src/document/interpolation-at.ts +134 -0
- package/src/document/names-in-scope.ts +126 -0
- package/src/document/resolve-fragment.ts +38 -0
- package/src/document/resolve-imported.ts +35 -0
- package/src/effects/index.ts +1 -0
- package/src/effects/waiting-fns.ts +83 -0
- package/src/env/env-vars.ts +88 -0
- package/src/env/env.types.ts +9 -0
- package/src/env/index.ts +3 -0
- package/src/env/render-env.ts +25 -0
- package/src/formatting/formatter.ts +68 -0
- package/src/formatting/index.ts +1 -0
- package/src/hover/hover.ts +322 -0
- package/src/hover/index.ts +1 -0
- package/src/hover/keywords.ts +168 -0
- package/src/hover/render-action.ts +94 -0
- package/src/hover/render-decl.ts +155 -0
- package/src/hover/render-imported.ts +49 -0
- package/src/hover/render-symbol.ts +108 -0
- package/src/hover/render-type-name.ts +78 -0
- package/src/hover/resolve-symbol.ts +77 -0
- package/src/hover/type-hover.ts +62 -0
- package/src/index.ts +38 -0
- package/src/markdown/index.ts +1 -0
- package/src/markdown/markdown.ts +38 -0
- package/src/references/document-highlight.ts +45 -0
- package/src/references/find-occurrences.ts +49 -0
- package/src/references/index.ts +13 -0
- package/src/references/name-property.ts +14 -0
- package/src/references/occurrence-range.ts +23 -0
- package/src/references/occurrences.ts +124 -0
- package/src/references/references.ts +38 -0
- package/src/references/symbol-at.ts +114 -0
- package/src/references/symbol.types.ts +38 -0
- package/src/rename/declares-deco.ts +15 -0
- package/src/rename/index.ts +1 -0
- package/src/rename/rename.ts +73 -0
- package/src/semantic/highlight-calls.ts +56 -0
- package/src/semantic/highlight-interpolation.ts +135 -0
- package/src/semantic/highlight-keywords.ts +115 -0
- package/src/semantic/highlight-literals.ts +38 -0
- package/src/semantic/highlight-module.ts +36 -0
- package/src/semantic/highlight-names.ts +97 -0
- package/src/semantic/highlight-paths.ts +80 -0
- package/src/semantic/highlight.types.ts +10 -0
- package/src/semantic/index.ts +1 -0
- package/src/semantic/semantic-tokens.ts +32 -0
- package/src/server/index.ts +1 -0
- package/src/server/start-server.ts +15 -0
- package/src/services/index.ts +2 -0
- package/src/services/lsp.types.ts +14 -0
- package/src/services/venn-lsp-module.ts +65 -0
- package/src/signature/action-call.ts +82 -0
- package/src/signature/bare-call.ts +55 -0
- package/src/signature/call-shape.ts +90 -0
- package/src/signature/call-shape.types.ts +34 -0
- package/src/signature/declared-shape.ts +52 -0
- package/src/signature/index.ts +11 -0
- package/src/signature/paren-call.ts +77 -0
- package/src/signature/render-shape.ts +92 -0
- package/src/signature/shape-at.ts +35 -0
- package/src/signature/signature-help.ts +98 -0
- package/src/symbols/document-symbols.ts +68 -0
- package/src/symbols/index.ts +1 -0
- package/src/testing/fixtures/alias-import.vn +16 -0
- package/src/testing/fixtures/shared/auth.vn +33 -0
- package/src/testing/fixtures/venn.toml +8 -0
- package/src/testing/lsp-fixture.ts +77 -0
- package/src/types/index.ts +3 -0
- package/src/types/type-service.ts +144 -0
- package/src/types/warm-types.ts +30 -0
- package/src/validation/check-validator.ts +111 -0
- package/src/validation/index.ts +1 -0
- package/src/workspace/import-resolver.ts +187 -0
- package/src/workspace/index.ts +1 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ParamSpec } from "@venn-lang/sdk";
|
|
2
|
+
import type { ParameterInformation, SignatureInformation } from "vscode-languageserver";
|
|
3
|
+
import type { CallShape, ShownArg } from "./call-shape.types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A call written out the way it is actually written: `http.on server handler`,
|
|
7
|
+
* no brackets, no commas. Each argument carries the character range it occupies
|
|
8
|
+
* in that line, which is what lets the editor bold the one being typed instead
|
|
9
|
+
* of guessing by substring.
|
|
10
|
+
*/
|
|
11
|
+
export function signatureOfShape(shape: CallShape): SignatureInformation {
|
|
12
|
+
return render(shape, { open: " ", separator: " ", close: "" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The same call, written the way a bracketed one is: `saudacao(nome: a, idade: b)`.
|
|
17
|
+
*
|
|
18
|
+
* Two renderings because the language has two call syntaxes, and a hint that
|
|
19
|
+
* shows the wrong one teaches the wrong thing.
|
|
20
|
+
*/
|
|
21
|
+
export function bracketed(shape: CallShape): SignatureInformation {
|
|
22
|
+
return render(shape, { open: "(", separator: ", ", close: ")" });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Punctuation {
|
|
26
|
+
open: string;
|
|
27
|
+
separator: string;
|
|
28
|
+
close: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One renderer for both syntaxes: only the punctuation between the parts
|
|
33
|
+
* differs, and the options are the last part in either. They are an argument
|
|
34
|
+
* like any other, so they get a range and documentation of their own rather
|
|
35
|
+
* than being printed as decoration nobody can point at.
|
|
36
|
+
*/
|
|
37
|
+
function render(shape: CallShape, punctuation: Punctuation): SignatureInformation {
|
|
38
|
+
const texts = [...shape.args.map(label), ...optionsPart(shape)];
|
|
39
|
+
const parameters: ParameterInformation[] = [];
|
|
40
|
+
let at = shape.target.length + punctuation.open.length;
|
|
41
|
+
texts.forEach((text, index) => {
|
|
42
|
+
parameters.push({ label: [at, at + text.length], documentation: docFor(shape, index) });
|
|
43
|
+
at += text.length + punctuation.separator.length;
|
|
44
|
+
});
|
|
45
|
+
const body = texts.join(punctuation.separator);
|
|
46
|
+
return {
|
|
47
|
+
label: `${shape.target}${texts.length ? punctuation.open : ""}${body}${texts.length ? punctuation.close : ""}`,
|
|
48
|
+
parameters,
|
|
49
|
+
documentation: summary(shape),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The options, when the verb accepts any: one last argument, always a map. */
|
|
54
|
+
function optionsPart(shape: CallShape): string[] {
|
|
55
|
+
return shape.options.length > 0 ? ["{ … }"] : [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** An argument's own line, or, for the last part, the keys the map accepts. */
|
|
59
|
+
function docFor(shape: CallShape, index: number): string | undefined {
|
|
60
|
+
const arg = shape.args[index];
|
|
61
|
+
if (arg) return arg.doc;
|
|
62
|
+
return shape.options.map(optionLine).join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function optionLine(spec: ParamSpec): string {
|
|
66
|
+
const required = spec.required ? " *(required)*" : "";
|
|
67
|
+
const doc = spec.doc ? ` — ${spec.doc}` : "";
|
|
68
|
+
return `- \`${spec.name}\`: \`${spec.type}\`${required}${doc}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `server: HttpServer`, or `reason?: string` when the call works without it.
|
|
73
|
+
* An argument nobody named shows as its type alone, rather than as a stray colon.
|
|
74
|
+
*/
|
|
75
|
+
function label(arg: ShownArg): string {
|
|
76
|
+
if (!arg.name) return arg.type;
|
|
77
|
+
return `${arg.name}${mark(arg)}: ${arg.type}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mark(arg: ShownArg): string {
|
|
81
|
+
if (arg.rest) return "…";
|
|
82
|
+
return arg.optional ? "?" : "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The line under the signature. It says what the verb does and what it gives
|
|
87
|
+
* back: the two questions someone halfway through typing it is asking.
|
|
88
|
+
*/
|
|
89
|
+
function summary(shape: CallShape): string | undefined {
|
|
90
|
+
const returns = shape.returns ? `Gives back ${shape.returns}.` : undefined;
|
|
91
|
+
return [shape.doc, returns].filter(Boolean).join(" ") || undefined;
|
|
92
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AstNode } from "@venn-lang/core";
|
|
2
|
+
import type { LangiumDocument } from "langium";
|
|
3
|
+
import type { SymbolCatalog } from "../catalog/index.js";
|
|
4
|
+
import { findBinding } from "../document/index.js";
|
|
5
|
+
import type { TypeService } from "../types/index.js";
|
|
6
|
+
import { callShape } from "./call-shape.js";
|
|
7
|
+
import type { CallShape } from "./call-shape.types.js";
|
|
8
|
+
import { declaredShape } from "./declared-shape.js";
|
|
9
|
+
|
|
10
|
+
export interface ShapeAtArgs {
|
|
11
|
+
/** The dotted name being called. */
|
|
12
|
+
path: string;
|
|
13
|
+
/** A node in the calling scope, for looking that name up. */
|
|
14
|
+
host: AstNode | undefined;
|
|
15
|
+
document: LangiumDocument;
|
|
16
|
+
catalog: SymbolCatalog;
|
|
17
|
+
types: TypeService;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What the callee of `f(…)` takes.
|
|
22
|
+
*
|
|
23
|
+
* One resolver for every kind of callee, so a call reads the same whoever wrote
|
|
24
|
+
* it. A name the file bound wins (a `fn`, a `fragment`, a `const` holding a
|
|
25
|
+
* lambda); otherwise the prelude and the loaded plugins answer. That a local
|
|
26
|
+
* name beats a namespace is the rule the evaluator follows too.
|
|
27
|
+
*/
|
|
28
|
+
export function shapeAt(args: ShapeAtArgs): CallShape | undefined {
|
|
29
|
+
const head = args.path.split(".")[0];
|
|
30
|
+
if (!head) return undefined;
|
|
31
|
+
const binding = args.host && findBinding(args.host, head);
|
|
32
|
+
if (!binding) return callShape(args.path, args.catalog);
|
|
33
|
+
if (args.path !== head) return undefined;
|
|
34
|
+
return declaredShape({ binding, name: head, document: args.document, types: args.types });
|
|
35
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { LangiumDocument } from "langium";
|
|
2
|
+
import type { SignatureHelpProvider } from "langium/lsp";
|
|
3
|
+
import type {
|
|
4
|
+
SignatureHelp,
|
|
5
|
+
SignatureHelpOptions,
|
|
6
|
+
SignatureHelpParams,
|
|
7
|
+
} from "vscode-languageserver";
|
|
8
|
+
import type { SymbolCatalog } from "../catalog/index.js";
|
|
9
|
+
import type { VennServices } from "../services/lsp.types.js";
|
|
10
|
+
import type { TypeService } from "../types/index.js";
|
|
11
|
+
import { activeArg, enclosingMatcher } from "./action-call.js";
|
|
12
|
+
import { enclosingBareCall } from "./bare-call.js";
|
|
13
|
+
import { callShape, matcherShape } from "./call-shape.js";
|
|
14
|
+
import type { CallShape } from "./call-shape.types.js";
|
|
15
|
+
import { enclosingParenCall } from "./paren-call.js";
|
|
16
|
+
import { bracketed, signatureOfShape } from "./render-shape.js";
|
|
17
|
+
import { shapeAt } from "./shape-at.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What the call being typed takes, one argument at a time.
|
|
21
|
+
*
|
|
22
|
+
* The space is a trigger character because Venn's calls have no brackets to
|
|
23
|
+
* hang one off: `http.on ` is already a call with an argument due. Waiting for
|
|
24
|
+
* a `(` that never comes is how a language ends up unable to explain itself.
|
|
25
|
+
*/
|
|
26
|
+
export class VennSignatureHelpProvider implements SignatureHelpProvider {
|
|
27
|
+
private readonly catalog: SymbolCatalog;
|
|
28
|
+
private readonly types: TypeService;
|
|
29
|
+
|
|
30
|
+
constructor(services: VennServices) {
|
|
31
|
+
this.catalog = services.catalog;
|
|
32
|
+
this.types = services.types;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get signatureHelpOptions(): SignatureHelpOptions {
|
|
36
|
+
return { triggerCharacters: ["(", ",", " "], retriggerCharacters: [" ", ","] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
provideSignatureHelp(
|
|
40
|
+
document: LangiumDocument,
|
|
41
|
+
params: SignatureHelpParams,
|
|
42
|
+
): SignatureHelp | undefined {
|
|
43
|
+
const offset = document.textDocument.offsetAt(params.position);
|
|
44
|
+
return (
|
|
45
|
+
this.forParens(document, offset) ??
|
|
46
|
+
this.forMatcher(document, offset) ??
|
|
47
|
+
this.forAction(document, offset)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `saudacao(▮)`: a call with brackets, wherever its callee was declared. */
|
|
52
|
+
private forParens(document: LangiumDocument, offset: number): SignatureHelp | undefined {
|
|
53
|
+
const found = enclosingParenCall(document, offset);
|
|
54
|
+
if (!found) return undefined;
|
|
55
|
+
const shape = shapeAt({ ...found, document, catalog: this.catalog, types: this.types });
|
|
56
|
+
if (!shape || parts(shape) === 0) return undefined;
|
|
57
|
+
return help(bracketed(shape), found.active, parts(shape));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `expect res contains ▮`: the value the check is waiting for. */
|
|
61
|
+
private forMatcher(document: LangiumDocument, offset: number): SignatureHelp | undefined {
|
|
62
|
+
const clause = enclosingMatcher(document, offset);
|
|
63
|
+
if (!clause) return undefined;
|
|
64
|
+
const shape = matcherShape(clause.name, this.catalog);
|
|
65
|
+
if (!shape || parts(shape) === 0) return undefined;
|
|
66
|
+
return help(signatureOfShape(shape), activeArg(clause, offset), parts(shape));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `http.on api handler`: a verb and its bare arguments, bound or not. */
|
|
70
|
+
private forAction(document: LangiumDocument, offset: number): SignatureHelp | undefined {
|
|
71
|
+
const call = enclosingBareCall(document, offset);
|
|
72
|
+
if (!call) return undefined;
|
|
73
|
+
const shape = callShape(call.target, this.catalog);
|
|
74
|
+
if (!shape || parts(shape) === 0) return undefined;
|
|
75
|
+
return help(signatureOfShape(shape), activeArg(call, offset), parts(shape));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How many things the signature can point at: the positional arguments, plus
|
|
81
|
+
* the options map when there is one. The map is the last argument, not
|
|
82
|
+
* decoration.
|
|
83
|
+
*/
|
|
84
|
+
function parts(shape: CallShape): number {
|
|
85
|
+
return shape.args.length + (shape.options.length > 0 ? 1 : 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function help(
|
|
89
|
+
signature: SignatureHelp["signatures"][number],
|
|
90
|
+
active: number,
|
|
91
|
+
count: number,
|
|
92
|
+
): SignatureHelp {
|
|
93
|
+
return {
|
|
94
|
+
signatures: [signature],
|
|
95
|
+
activeSignature: 0,
|
|
96
|
+
activeParameter: Math.min(active, Math.max(count - 1, 0)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AstNode,
|
|
3
|
+
type Block,
|
|
4
|
+
type Document,
|
|
5
|
+
isDecoDecl,
|
|
6
|
+
isFlowDecl,
|
|
7
|
+
isFragmentDecl,
|
|
8
|
+
isGroupDecl,
|
|
9
|
+
isStepDecl,
|
|
10
|
+
} from "@venn-lang/core";
|
|
11
|
+
import type { LangiumDocument } from "langium";
|
|
12
|
+
import type { DocumentSymbolProvider } from "langium/lsp";
|
|
13
|
+
import { type DocumentSymbol, SymbolKind } from "vscode-languageserver";
|
|
14
|
+
|
|
15
|
+
const ORIGIN = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
|
|
16
|
+
|
|
17
|
+
/** The outline: flows with their steps and groups, plus fragments and resources. */
|
|
18
|
+
export class VennDocumentSymbolProvider implements DocumentSymbolProvider {
|
|
19
|
+
getSymbols(document: LangiumDocument): DocumentSymbol[] {
|
|
20
|
+
const root = document.parseResult?.value as Document | undefined;
|
|
21
|
+
return root ? root.decls.flatMap(topSymbol) : [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function topSymbol(decl: AstNode): DocumentSymbol[] {
|
|
26
|
+
if (isFlowDecl(decl)) {
|
|
27
|
+
const children = blockSymbols(decl.body);
|
|
28
|
+
return [symbol({ node: decl, name: decl.title, kind: SymbolKind.Event, children })];
|
|
29
|
+
}
|
|
30
|
+
if (isFragmentDecl(decl)) {
|
|
31
|
+
return [symbol({ node: decl, name: decl.name, kind: SymbolKind.Function })];
|
|
32
|
+
}
|
|
33
|
+
// A `deco` is a declaration a reader looks for by name, like a fragment.
|
|
34
|
+
if (isDecoDecl(decl)) {
|
|
35
|
+
return [symbol({ node: decl, name: decl.name, kind: SymbolKind.Function })];
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function blockSymbols(block: Block): DocumentSymbol[] {
|
|
41
|
+
return block.stmts.flatMap((stmt) => {
|
|
42
|
+
if (isStepDecl(stmt)) {
|
|
43
|
+
const children = blockSymbols(stmt.body);
|
|
44
|
+
return [symbol({ node: stmt, name: stmt.title, kind: SymbolKind.Method, children })];
|
|
45
|
+
}
|
|
46
|
+
if (isGroupDecl(stmt)) {
|
|
47
|
+
const children = blockSymbols(stmt.body);
|
|
48
|
+
return [symbol({ node: stmt, name: stmt.title, kind: SymbolKind.Namespace, children })];
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function symbol(args: {
|
|
55
|
+
node: AstNode;
|
|
56
|
+
name: string;
|
|
57
|
+
kind: SymbolKind;
|
|
58
|
+
children?: DocumentSymbol[];
|
|
59
|
+
}): DocumentSymbol {
|
|
60
|
+
const range = args.node.$cstNode?.range ?? ORIGIN;
|
|
61
|
+
return {
|
|
62
|
+
name: args.name,
|
|
63
|
+
kind: args.kind,
|
|
64
|
+
range,
|
|
65
|
+
selectionRange: range,
|
|
66
|
+
children: args.children,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VennDocumentSymbolProvider } from "./document-symbols.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Imports through a `#alias` path declared in [paths] of venn.toml.
|
|
2
|
+
module fixtures.aliasimport
|
|
3
|
+
|
|
4
|
+
use "venn/assert"
|
|
5
|
+
use "venn/http"
|
|
6
|
+
|
|
7
|
+
import { authHeader, login } from "#shared/auth.vn"
|
|
8
|
+
|
|
9
|
+
flow "Alias import" {
|
|
10
|
+
run login("alice")
|
|
11
|
+
step "after login" {
|
|
12
|
+
expect authHeader("abc123") == "Bearer abc123"
|
|
13
|
+
http.get "https://example.com/dashboard"
|
|
14
|
+
expect true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module fixtures.shared.auth
|
|
2
|
+
|
|
3
|
+
use "venn/http"
|
|
4
|
+
use "venn/assert"
|
|
5
|
+
|
|
6
|
+
## Signs a user in through the API and asserts the session is live.
|
|
7
|
+
##
|
|
8
|
+
## Put it at the top of any flow that needs an authenticated account. The
|
|
9
|
+
## response stays available to the caller as `res`.
|
|
10
|
+
##
|
|
11
|
+
## @param user The account name to sign in as.
|
|
12
|
+
## @returns The HTTP response of the sign-in call.
|
|
13
|
+
## @example
|
|
14
|
+
## run login("alice")
|
|
15
|
+
## expect res.status == 200
|
|
16
|
+
pub fragment login(user) {
|
|
17
|
+
step "login" {
|
|
18
|
+
let res = http.get "https://example.com"
|
|
19
|
+
expect res.status == 200
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
## Builds the Authorization header a signed-in request carries.
|
|
24
|
+
##
|
|
25
|
+
## A `pub fn` crosses files the same way a fragment does: it is a value, so it
|
|
26
|
+
## can be called, passed and held, and it runs inside *this* file, where
|
|
27
|
+
## `SCHEME` below is private and never leaves.
|
|
28
|
+
##
|
|
29
|
+
## @param token The session token to send.
|
|
30
|
+
## @returns The header value, scheme included.
|
|
31
|
+
pub fn authHeader(token: string) -> string => "${SCHEME} ${token}"
|
|
32
|
+
|
|
33
|
+
const SCHEME = "Bearer"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { EmptyFileSystem, type LangiumDocument, URI } from "langium";
|
|
3
|
+
import type { LangiumSharedServices } from "langium/lsp";
|
|
4
|
+
import { NodeFileSystem } from "langium/node";
|
|
5
|
+
import type { Position, TextEdit } from "vscode-languageserver";
|
|
6
|
+
import { createVennLspServices } from "../services/index.js";
|
|
7
|
+
import type { VennServices } from "../services/lsp.types.js";
|
|
8
|
+
|
|
9
|
+
/** A built language service plus one parsed, validated in-memory document. */
|
|
10
|
+
export interface Fixture {
|
|
11
|
+
services: VennServices;
|
|
12
|
+
document: LangiumDocument;
|
|
13
|
+
uri: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Three slashes: the path must carry the `.vn` extension for the service registry.
|
|
17
|
+
const FOLDER = "memory:///";
|
|
18
|
+
const NAME = "test.vn";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the services and a validated document: the seam every LSP test drives.
|
|
22
|
+
*
|
|
23
|
+
* `modules` are files placed beside it, keyed by file name, so an `import …
|
|
24
|
+
* from "./lib.vn"` resolves without a disk anywhere in sight.
|
|
25
|
+
*/
|
|
26
|
+
export async function fixture(
|
|
27
|
+
source: string,
|
|
28
|
+
modules: Record<string, string> = {},
|
|
29
|
+
): Promise<Fixture> {
|
|
30
|
+
const { shared, Venn } = createVennLspServices(EmptyFileSystem);
|
|
31
|
+
const beside = Object.entries(modules).map(([name, text]) => added(shared, name, text));
|
|
32
|
+
const document = added(shared, NAME, source);
|
|
33
|
+
await shared.workspace.DocumentBuilder.build([...beside, document], { validation: true });
|
|
34
|
+
// The document's own string form: vscode-uri normalises, so never re-derive it.
|
|
35
|
+
return { services: Venn, document, uri: document.uri.toString() };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function added(shared: LangiumSharedServices, name: string, source: string): LangiumDocument {
|
|
39
|
+
const uri = URI.parse(`${FOLDER}${name}`);
|
|
40
|
+
const document = shared.workspace.LangiumDocumentFactory.fromString(source, uri);
|
|
41
|
+
shared.workspace.LangiumDocuments.addDocument(document);
|
|
42
|
+
return document;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Same, but for a real file on disk, on the node filesystem, so `venn.toml`,
|
|
47
|
+
* `#alias` paths and imported documents all resolve the way they do in an editor.
|
|
48
|
+
*/
|
|
49
|
+
export async function fixtureFromFile(path: string): Promise<Fixture> {
|
|
50
|
+
const { shared, Venn } = createVennLspServices(NodeFileSystem);
|
|
51
|
+
const uri = URI.file(path);
|
|
52
|
+
const source = readFileSync(path, "utf8");
|
|
53
|
+
const document = shared.workspace.LangiumDocumentFactory.fromString(source, uri);
|
|
54
|
+
shared.workspace.LangiumDocuments.addDocument(document);
|
|
55
|
+
await shared.workspace.DocumentBuilder.build([document], { validation: false });
|
|
56
|
+
return { services: Venn, document, uri: document.uri.toString() };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Apply edits to the document's text, last-to-first so earlier offsets stay valid. */
|
|
60
|
+
export function applyEdits(document: LangiumDocument, edits: readonly TextEdit[]): string {
|
|
61
|
+
const text = document.textDocument;
|
|
62
|
+
const offset = (edit: TextEdit): number => text.offsetAt(edit.range.start);
|
|
63
|
+
const sorted = [...edits].sort((a, b) => offset(b) - offset(a));
|
|
64
|
+
let out = text.getText();
|
|
65
|
+
for (const edit of sorted) {
|
|
66
|
+
out = out.slice(0, offset(edit)) + edit.newText + out.slice(text.offsetAt(edit.range.end));
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The position where `needle` starts, which is inside its first token, even for
|
|
73
|
+
* one-character names like `x`.
|
|
74
|
+
*/
|
|
75
|
+
export function positionOf(document: LangiumDocument, needle: string): Position {
|
|
76
|
+
return document.textDocument.positionAt(document.textDocument.getText().indexOf(needle));
|
|
77
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AstNode,
|
|
3
|
+
checkTypes,
|
|
4
|
+
type Document,
|
|
5
|
+
type Expr,
|
|
6
|
+
type ImportedDeco,
|
|
7
|
+
importedTypes,
|
|
8
|
+
isPackageSpecifier,
|
|
9
|
+
isValueImport,
|
|
10
|
+
type Problem,
|
|
11
|
+
type Type,
|
|
12
|
+
type TypeCatalog,
|
|
13
|
+
} from "@venn-lang/core";
|
|
14
|
+
import { createTypeCatalog } from "@venn-lang/runtime";
|
|
15
|
+
import { allPlugins } from "@venn-lang/stdlib";
|
|
16
|
+
import type { TypeSpec } from "@venn-lang/types";
|
|
17
|
+
import type { LangiumDocument } from "langium";
|
|
18
|
+
import { importedDecos } from "../deco/index.js";
|
|
19
|
+
import { importedModules, type ModuleGraph } from "../document/index.js";
|
|
20
|
+
import type { VennServices } from "../services/lsp.types.js";
|
|
21
|
+
|
|
22
|
+
/** What inference found for one document: its type errors and every node's type. */
|
|
23
|
+
export interface DocumentTypes {
|
|
24
|
+
problems: readonly Problem[];
|
|
25
|
+
types: ReadonlyMap<AstNode, Type>;
|
|
26
|
+
/**
|
|
27
|
+
* Per string literal, the expression inference parsed from each `${…}`. The
|
|
28
|
+
* document's own tree stops at the string, so these are the only nodes the
|
|
29
|
+
* editor can reach for code written inside one.
|
|
30
|
+
*/
|
|
31
|
+
slots: ReadonlyMap<AstNode, readonly (Expr | undefined)[]>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Type information for the workspace, computed once per parse of a document.
|
|
36
|
+
*
|
|
37
|
+
* Inference walks a whole file, and several features want its result:
|
|
38
|
+
* diagnostics on every edit, hover on every mouse move. Without a shared cache
|
|
39
|
+
* the same file is re-checked many times per keystroke; with it, a file is
|
|
40
|
+
* checked once when it changes and every reader is served from memory.
|
|
41
|
+
*/
|
|
42
|
+
export interface TypeService {
|
|
43
|
+
/** Everything inference knows about this document, cached until it is reparsed. */
|
|
44
|
+
of(document: LangiumDocument): DocumentTypes;
|
|
45
|
+
/** What is already cached, or undefined, for readers that must not block. */
|
|
46
|
+
peek(document: LangiumDocument): DocumentTypes | undefined;
|
|
47
|
+
/** Forget a document: it was deleted, or its folder was closed. */
|
|
48
|
+
forget(uri: string): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const EMPTY: DocumentTypes = { problems: [], types: new Map(), slots: new Map() };
|
|
52
|
+
|
|
53
|
+
interface Entry {
|
|
54
|
+
/** The AST this was computed from. A reparse yields a new root, so a stale
|
|
55
|
+
* entry can never be mistaken for a fresh one. A version number could be,
|
|
56
|
+
* since replacing a document resets it to zero. */
|
|
57
|
+
root: object;
|
|
58
|
+
result: DocumentTypes;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createTypeService(services: VennServices): TypeService {
|
|
62
|
+
const cache = new Map<string, Entry>();
|
|
63
|
+
// Built once: reading every plugin's published types on each keystroke would
|
|
64
|
+
// be work the answer never changes for.
|
|
65
|
+
const catalog = createTypeCatalog(allPlugins);
|
|
66
|
+
return {
|
|
67
|
+
of(document) {
|
|
68
|
+
const cached = hit(cache, document);
|
|
69
|
+
if (cached) return cached;
|
|
70
|
+
const root = document.parseResult?.value;
|
|
71
|
+
const result = compute({ document, catalog, ...outside(document, services) });
|
|
72
|
+
if (root) cache.set(document.uri.toString(), { root, result });
|
|
73
|
+
return result;
|
|
74
|
+
},
|
|
75
|
+
peek: (document) => hit(cache, document),
|
|
76
|
+
forget: (uri) => void cache.delete(uri),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The `pub deco`s this file imports.
|
|
82
|
+
*
|
|
83
|
+
* Read here rather than in {@link compute} because the services are resolved
|
|
84
|
+
* lazily: touching them while the module is still being built would close a
|
|
85
|
+
* loop that only opens once everything is constructed.
|
|
86
|
+
*/
|
|
87
|
+
function outside(
|
|
88
|
+
document: LangiumDocument,
|
|
89
|
+
services: VennServices,
|
|
90
|
+
): {
|
|
91
|
+
decos?: Map<string, ImportedDeco>;
|
|
92
|
+
graph?: ModuleGraph;
|
|
93
|
+
packages?: Map<string, Record<string, TypeSpec>>;
|
|
94
|
+
} {
|
|
95
|
+
const root = document.parseResult?.value as Document | undefined;
|
|
96
|
+
if (!root) return {};
|
|
97
|
+
const scope = {
|
|
98
|
+
root,
|
|
99
|
+
uri: document.uri,
|
|
100
|
+
documents: services.shared.workspace.LangiumDocuments,
|
|
101
|
+
imports: services.imports,
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
decos: importedDecos(scope),
|
|
105
|
+
graph: importedModules(scope),
|
|
106
|
+
packages: services.imports.packageTypes(document.uri, packagesNamed(root)),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The bare specifiers this file imports: the ones that name a package. */
|
|
111
|
+
function packagesNamed(root: Document): string[] {
|
|
112
|
+
return root.imports
|
|
113
|
+
.filter(isValueImport)
|
|
114
|
+
.map((decl) => decl.path)
|
|
115
|
+
.filter(isPackageSpecifier);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hit(cache: Map<string, Entry>, document: LangiumDocument): DocumentTypes | undefined {
|
|
119
|
+
const entry = cache.get(document.uri.toString());
|
|
120
|
+
return entry && entry.root === document.parseResult?.value ? entry.result : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function compute(args: {
|
|
124
|
+
document: LangiumDocument;
|
|
125
|
+
catalog: TypeCatalog;
|
|
126
|
+
decos?: Map<string, ImportedDeco>;
|
|
127
|
+
graph?: ModuleGraph;
|
|
128
|
+
packages?: Map<string, Record<string, TypeSpec>>;
|
|
129
|
+
}): DocumentTypes {
|
|
130
|
+
const { document, catalog, decos, graph } = args;
|
|
131
|
+
const root = document.parseResult?.value as Document | undefined;
|
|
132
|
+
if (!root) return EMPTY;
|
|
133
|
+
const uri = document.uri.toString();
|
|
134
|
+
// What the imported names are, worked out from the files they were written
|
|
135
|
+
// in, so an imported function has a hover and its arguments are checked.
|
|
136
|
+
const imports =
|
|
137
|
+
graph && importedTypes({ ...graph, document: root, uri, catalog, packages: args.packages });
|
|
138
|
+
const checked = checkTypes(root, { uri, catalog, decos, imports });
|
|
139
|
+
return {
|
|
140
|
+
problems: checked.problems,
|
|
141
|
+
types: checked.types as ReadonlyMap<AstNode, Type>,
|
|
142
|
+
slots: checked.slots as ReadonlyMap<AstNode, readonly (Expr | undefined)[]>,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DocumentState, type LangiumDocument, type URI } from "langium";
|
|
2
|
+
import type { LangiumSharedServices } from "langium/lsp";
|
|
3
|
+
import type { TypeService } from "./type-service.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Keep the workspace's types warm.
|
|
7
|
+
*
|
|
8
|
+
* Langium already indexes every `.vn` in the opened folder, and re-indexes a
|
|
9
|
+
* file when it changes. Hooking the same cycle means the project is fully typed
|
|
10
|
+
* by the time you touch anything, and a keystroke re-types only the file you
|
|
11
|
+
* edited, never the rest.
|
|
12
|
+
*/
|
|
13
|
+
export function warmTypes(shared: LangiumSharedServices, types: TypeService): void {
|
|
14
|
+
const builder = shared.workspace.DocumentBuilder;
|
|
15
|
+
builder.onBuildPhase(DocumentState.IndexedContent, (documents: LangiumDocument[]) => {
|
|
16
|
+
for (const document of documents) warm(document, types);
|
|
17
|
+
});
|
|
18
|
+
builder.onUpdate((_changed: URI[], deleted: URI[]) => {
|
|
19
|
+
for (const uri of deleted) types.forget(uri.toString());
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Inference must never take the server down: a bad file loses its types, nothing more. */
|
|
24
|
+
function warm(document: LangiumDocument, types: TypeService): void {
|
|
25
|
+
try {
|
|
26
|
+
types.of(document);
|
|
27
|
+
} catch {
|
|
28
|
+
types.forget(document.uri.toString());
|
|
29
|
+
}
|
|
30
|
+
}
|