@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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/dist/bin/venn-lsp.d.mts +1 -0
  4. package/dist/bin/venn-lsp.mjs +17 -0
  5. package/dist/bin/venn-lsp.mjs.map +1 -0
  6. package/dist/index.d.mts +722 -0
  7. package/dist/index.d.mts.map +1 -0
  8. package/dist/index.mjs +2 -0
  9. package/dist/server-BET60xwK.mjs +5376 -0
  10. package/dist/server-BET60xwK.mjs.map +1 -0
  11. package/package.json +65 -0
  12. package/src/bin/venn-lsp.ts +17 -0
  13. package/src/catalog/build-catalog.ts +72 -0
  14. package/src/catalog/catalog.types.ts +44 -0
  15. package/src/catalog/index.ts +2 -0
  16. package/src/code-actions/anchor.ts +49 -0
  17. package/src/code-actions/code-actions.ts +139 -0
  18. package/src/code-actions/exporting-modules.ts +73 -0
  19. package/src/code-actions/index.ts +5 -0
  20. package/src/completion/argument-items.ts +81 -0
  21. package/src/completion/completion.ts +416 -0
  22. package/src/completion/completion.types.ts +38 -0
  23. package/src/completion/context.ts +144 -0
  24. package/src/completion/icons.ts +83 -0
  25. package/src/completion/index.ts +4 -0
  26. package/src/completion/items.ts +232 -0
  27. package/src/completion/member-items.ts +147 -0
  28. package/src/completion/module-paths.ts +63 -0
  29. package/src/completion/options-map-items.ts +47 -0
  30. package/src/completion/read-from.ts +77 -0
  31. package/src/completion/type-name-items.ts +62 -0
  32. package/src/deco/builtin-decos.ts +35 -0
  33. package/src/deco/builtin-docs.ts +26 -0
  34. package/src/deco/declared-deco.ts +56 -0
  35. package/src/deco/deco.types.ts +23 -0
  36. package/src/deco/decos-in-scope.ts +63 -0
  37. package/src/deco/imported-decos.ts +49 -0
  38. package/src/deco/index.ts +9 -0
  39. package/src/deco/kind-of.ts +15 -0
  40. package/src/deco/render-deco.ts +44 -0
  41. package/src/definition/definition.ts +93 -0
  42. package/src/definition/index.ts +1 -0
  43. package/src/docs/doc.types.ts +17 -0
  44. package/src/docs/index.ts +4 -0
  45. package/src/docs/parse-doc.ts +42 -0
  46. package/src/docs/read-doc.ts +45 -0
  47. package/src/docs/render-doc.ts +42 -0
  48. package/src/document/exported-names.ts +30 -0
  49. package/src/document/expression-path.ts +43 -0
  50. package/src/document/find-binding.ts +82 -0
  51. package/src/document/host-at.ts +61 -0
  52. package/src/document/imported-modules.ts +91 -0
  53. package/src/document/imported-names.ts +15 -0
  54. package/src/document/index.ts +19 -0
  55. package/src/document/interpolation-at.ts +134 -0
  56. package/src/document/names-in-scope.ts +126 -0
  57. package/src/document/resolve-fragment.ts +38 -0
  58. package/src/document/resolve-imported.ts +35 -0
  59. package/src/effects/index.ts +1 -0
  60. package/src/effects/waiting-fns.ts +83 -0
  61. package/src/env/env-vars.ts +88 -0
  62. package/src/env/env.types.ts +9 -0
  63. package/src/env/index.ts +3 -0
  64. package/src/env/render-env.ts +25 -0
  65. package/src/formatting/formatter.ts +68 -0
  66. package/src/formatting/index.ts +1 -0
  67. package/src/hover/hover.ts +322 -0
  68. package/src/hover/index.ts +1 -0
  69. package/src/hover/keywords.ts +168 -0
  70. package/src/hover/render-action.ts +94 -0
  71. package/src/hover/render-decl.ts +155 -0
  72. package/src/hover/render-imported.ts +49 -0
  73. package/src/hover/render-symbol.ts +108 -0
  74. package/src/hover/render-type-name.ts +78 -0
  75. package/src/hover/resolve-symbol.ts +77 -0
  76. package/src/hover/type-hover.ts +62 -0
  77. package/src/index.ts +38 -0
  78. package/src/markdown/index.ts +1 -0
  79. package/src/markdown/markdown.ts +38 -0
  80. package/src/references/document-highlight.ts +45 -0
  81. package/src/references/find-occurrences.ts +49 -0
  82. package/src/references/index.ts +13 -0
  83. package/src/references/name-property.ts +14 -0
  84. package/src/references/occurrence-range.ts +23 -0
  85. package/src/references/occurrences.ts +124 -0
  86. package/src/references/references.ts +38 -0
  87. package/src/references/symbol-at.ts +114 -0
  88. package/src/references/symbol.types.ts +38 -0
  89. package/src/rename/declares-deco.ts +15 -0
  90. package/src/rename/index.ts +1 -0
  91. package/src/rename/rename.ts +73 -0
  92. package/src/semantic/highlight-calls.ts +56 -0
  93. package/src/semantic/highlight-interpolation.ts +135 -0
  94. package/src/semantic/highlight-keywords.ts +115 -0
  95. package/src/semantic/highlight-literals.ts +38 -0
  96. package/src/semantic/highlight-module.ts +36 -0
  97. package/src/semantic/highlight-names.ts +97 -0
  98. package/src/semantic/highlight-paths.ts +80 -0
  99. package/src/semantic/highlight.types.ts +10 -0
  100. package/src/semantic/index.ts +1 -0
  101. package/src/semantic/semantic-tokens.ts +32 -0
  102. package/src/server/index.ts +1 -0
  103. package/src/server/start-server.ts +15 -0
  104. package/src/services/index.ts +2 -0
  105. package/src/services/lsp.types.ts +14 -0
  106. package/src/services/venn-lsp-module.ts +65 -0
  107. package/src/signature/action-call.ts +82 -0
  108. package/src/signature/bare-call.ts +55 -0
  109. package/src/signature/call-shape.ts +90 -0
  110. package/src/signature/call-shape.types.ts +34 -0
  111. package/src/signature/declared-shape.ts +52 -0
  112. package/src/signature/index.ts +11 -0
  113. package/src/signature/paren-call.ts +77 -0
  114. package/src/signature/render-shape.ts +92 -0
  115. package/src/signature/shape-at.ts +35 -0
  116. package/src/signature/signature-help.ts +98 -0
  117. package/src/symbols/document-symbols.ts +68 -0
  118. package/src/symbols/index.ts +1 -0
  119. package/src/testing/fixtures/alias-import.vn +16 -0
  120. package/src/testing/fixtures/shared/auth.vn +33 -0
  121. package/src/testing/fixtures/venn.toml +8 -0
  122. package/src/testing/lsp-fixture.ts +77 -0
  123. package/src/types/index.ts +3 -0
  124. package/src/types/type-service.ts +144 -0
  125. package/src/types/warm-types.ts +30 -0
  126. package/src/validation/check-validator.ts +111 -0
  127. package/src/validation/index.ts +1 -0
  128. package/src/workspace/import-resolver.ts +187 -0
  129. 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,8 @@
1
+ # The manifest these fixtures resolve against. `#shared` is what the alias
2
+ # tests follow, so it has to be declared here rather than inherited.
3
+ [package]
4
+ name = "lsp-fixtures"
5
+ version = "0.0.0"
6
+
7
+ [paths]
8
+ "#shared" = "./shared"
@@ -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,3 @@
1
+ export type { DocumentTypes, TypeService } from "./type-service.js";
2
+ export { createTypeService } from "./type-service.js";
3
+ export { warmTypes } from "./warm-types.js";
@@ -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
+ }