@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,56 @@
1
+ import {
2
+ type DecoDecl,
3
+ type Document,
4
+ isDecoDecl,
5
+ isNamedType,
6
+ type Param,
7
+ type SingleType,
8
+ type TypeRef,
9
+ } from "@venn-lang/core";
10
+ import type { LangiumDocument } from "langium";
11
+ import { readDoc, renderDoc } from "../docs/index.js";
12
+ import type { DecoInfo } from "./deco.types.js";
13
+
14
+ /**
15
+ * Read a `deco` the way whoever writes `@name` sees it.
16
+ *
17
+ * The first parameter is the target, so the type written on it is what the
18
+ * decorator decorates. Nothing else in the declaration says so, and nothing
19
+ * outside it can disagree.
20
+ */
21
+ export function declaredDeco(args: { decl: DecoDecl; document: LangiumDocument }): DecoInfo {
22
+ const { decl, document } = args;
23
+ return {
24
+ name: decl.name,
25
+ decorates: typeNames(decl.params?.params[0]?.paramType),
26
+ signature: signatureOf(decl),
27
+ doc: renderDoc(readDoc(document, decl)),
28
+ decl,
29
+ document,
30
+ };
31
+ }
32
+
33
+ /** Every `deco` a document declares, in the order it declares them. */
34
+ export function localDecos(root: Document, document: LangiumDocument): DecoInfo[] {
35
+ return root.decls.filter(isDecoDecl).map((decl) => declaredDeco({ decl, document }));
36
+ }
37
+
38
+ function signatureOf(decl: DecoDecl): string {
39
+ const params = (decl.params?.params ?? []).map(paramText).join(", ");
40
+ return `${decl.export ? "pub " : ""}deco ${decl.name}(${params})`;
41
+ }
42
+
43
+ function paramText(param: Param): string {
44
+ const type = typeNames(param.paramType).join(" | ");
45
+ return type ? `${param.name}: ${type}` : param.name;
46
+ }
47
+
48
+ /** Each alternative of a written type: `Fn | Flow` is two kinds, not one. */
49
+ function typeNames(ref: TypeRef | undefined): string[] {
50
+ return (ref?.members ?? []).map(memberName).filter((name) => name.length > 0);
51
+ }
52
+
53
+ // The source's own text, so a generic like `list<number>` survives the trip.
54
+ function memberName(member: SingleType): string {
55
+ return member.$cstNode?.text ?? (isNamedType(member) ? member.name : "");
56
+ }
@@ -0,0 +1,23 @@
1
+ import type { DecoDecl } from "@venn-lang/core";
2
+ import type { LangiumDocument } from "langium";
3
+
4
+ /**
5
+ * One decorator a `@name` could mean here: a `deco` some file declares, or one
6
+ * the language ships with.
7
+ *
8
+ * `decorates` is in the language's own words (`Fn`, `Flow`, `Step`), never a
9
+ * node type. What a decorator applies to is part of its signature, so reading
10
+ * one costs nobody a lesson in the compiler's vocabulary.
11
+ */
12
+ export interface DecoInfo {
13
+ name: string;
14
+ /** The kinds it may sit on. Empty means it constrains nothing. */
15
+ decorates: readonly string[];
16
+ /** The line a hover fences: `pub deco retry(target: Flow, times: number)`. */
17
+ signature: string;
18
+ doc?: string;
19
+ /** The declaration, when a `.vn` declares it: where go-to-definition lands. */
20
+ decl?: DecoDecl;
21
+ /** The document holding {@link decl}. Absent for a built-in. */
22
+ document?: LangiumDocument;
23
+ }
@@ -0,0 +1,63 @@
1
+ import { type Document, isValueImport, type ValueImport } from "@venn-lang/core";
2
+ import type { LangiumDocument, LangiumDocuments } from "langium";
3
+ import type { ImportResolver } from "../workspace/index.js";
4
+ import { builtinDecos } from "./builtin-decos.js";
5
+ import { localDecos } from "./declared-deco.js";
6
+ import type { DecoInfo } from "./deco.types.js";
7
+
8
+ /** What a file needs consulted before it can say what `@name` means. */
9
+ export interface DecoScope {
10
+ document: LangiumDocument;
11
+ documents: LangiumDocuments;
12
+ imports: ImportResolver;
13
+ }
14
+
15
+ /**
16
+ * Every decorator a `@name` written in this file could mean: the ones the
17
+ * language ships with, the `pub deco`s it imported, and the `deco`s it declares
18
+ * itself.
19
+ *
20
+ * Later wins, on the same rule the runtime already follows for plugins: the
21
+ * built-ins are a stdlib, not a reserved word list, so a file that declares its
22
+ * own `deco retry` means its own.
23
+ */
24
+ export async function decosInScope(scope: DecoScope): Promise<DecoInfo[]> {
25
+ const root = rootOf(scope.document);
26
+ if (!root) return builtinDecos();
27
+ const imported = await importedDecos(root, scope);
28
+ return dedupe([...builtinDecos(), ...imported, ...localDecos(root, scope.document)]);
29
+ }
30
+
31
+ /** The decorator a `@name` resolves to, or nothing when no decorator carries it. */
32
+ export async function decoNamed(name: string, scope: DecoScope): Promise<DecoInfo | undefined> {
33
+ return (await decosInScope(scope)).find((info) => info.name === name);
34
+ }
35
+
36
+ async function importedDecos(root: Document, scope: DecoScope): Promise<DecoInfo[]> {
37
+ const found: DecoInfo[] = [];
38
+ for (const decl of root.imports) {
39
+ if (isValueImport(decl)) found.push(...(await fromModule(decl, scope)));
40
+ }
41
+ return found;
42
+ }
43
+
44
+ // Only what the other file marked `pub`, and only the names this one asked for.
45
+ async function fromModule(decl: ValueImport, scope: DecoScope): Promise<DecoInfo[]> {
46
+ const uri = scope.imports.resolve(decl.path, scope.document.uri);
47
+ const document = await scope.documents.getOrCreateDocument(uri).catch(() => undefined);
48
+ const root = document && rootOf(document);
49
+ if (!document || !root) return [];
50
+ return localDecos(root, document).filter(
51
+ (info) => info.decl?.export && decl.names.includes(info.name),
52
+ );
53
+ }
54
+
55
+ function dedupe(all: readonly DecoInfo[]): DecoInfo[] {
56
+ const byName = new Map<string, DecoInfo>();
57
+ for (const info of all) byName.set(info.name, info);
58
+ return [...byName.values()];
59
+ }
60
+
61
+ function rootOf(document: LangiumDocument): Document | undefined {
62
+ return document.parseResult?.value as Document | undefined;
63
+ }
@@ -0,0 +1,49 @@
1
+ import { type Document, type ImportedDeco, isDecoDecl, isValueImport } from "@venn-lang/core";
2
+ import type { LangiumDocument, LangiumDocuments, URI } from "langium";
3
+ import type { ImportResolver } from "../workspace/index.js";
4
+
5
+ /** What is needed to read a neighbour without waiting for it to load. */
6
+ export interface ImportedDecoScope {
7
+ root: Document;
8
+ uri: URI;
9
+ documents: LangiumDocuments;
10
+ imports: ImportResolver;
11
+ }
12
+
13
+ /**
14
+ * The `pub deco`s this file imports, read straight out of the workspace index.
15
+ *
16
+ * Synchronous on purpose: the checker runs on every keystroke and cannot await
17
+ * a file load. A neighbour the workspace has not indexed yet contributes
18
+ * nothing this pass and is picked up by the next build, which is a moment of
19
+ * silence rather than a wrong answer.
20
+ */
21
+ export function importedDecos(scope: ImportedDecoScope): Map<string, ImportedDeco> {
22
+ const found = new Map<string, ImportedDeco>();
23
+ for (const decl of scope.root.imports) {
24
+ if (isValueImport(decl)) collect({ scope, spec: decl.path, names: decl.names, found });
25
+ }
26
+ return found;
27
+ }
28
+
29
+ function collect(args: {
30
+ scope: ImportedDecoScope;
31
+ spec: string;
32
+ names: readonly string[];
33
+ found: Map<string, ImportedDeco>;
34
+ }): void {
35
+ const uri = args.scope.imports.resolve(args.spec, args.scope.uri);
36
+ const document = args.scope.documents.getDocument(uri);
37
+ const root = document && rootOf(document);
38
+ if (!root) return;
39
+ // Only what the other file marked `pub`, and only the names this one asked for.
40
+ for (const decl of root.decls) {
41
+ if (isDecoDecl(decl) && decl.export && args.names.includes(decl.name)) {
42
+ args.found.set(decl.name, { decl, uri: uri.toString() });
43
+ }
44
+ }
45
+ }
46
+
47
+ function rootOf(document: LangiumDocument): Document | undefined {
48
+ return document.parseResult?.value as Document | undefined;
49
+ }
@@ -0,0 +1,9 @@
1
+ export { builtinDecos } from "./builtin-decos.js";
2
+ export { declaredDeco, localDecos } from "./declared-deco.js";
3
+ export type { DecoInfo } from "./deco.types.js";
4
+ export type { DecoScope } from "./decos-in-scope.js";
5
+ export { decoNamed, decosInScope } from "./decos-in-scope.js";
6
+ export type { ImportedDecoScope } from "./imported-decos.js";
7
+ export { importedDecos } from "./imported-decos.js";
8
+ export { kindOf } from "./kind-of.js";
9
+ export { decoHover, decoratesLabel, documentedHover } from "./render-deco.js";
@@ -0,0 +1,15 @@
1
+ /** Where dropping the suffix would be wrong, because the user's word differs. */
2
+ const KINDS: Record<string, string> = {
3
+ LetStmt: "Binding",
4
+ CaptureStmt: "Binding",
5
+ };
6
+
7
+ /**
8
+ * The user's word for a node type.
9
+ *
10
+ * `targets: ["FlowDecl"]` is the compiler talking to itself. A decorator says
11
+ * it decorates a `Flow`, and that is the only spelling the editor shows.
12
+ */
13
+ export function kindOf(type: string): string {
14
+ return KINDS[type] ?? type.replace(/(Decl|Stmt|Expr)$/, "");
15
+ }
@@ -0,0 +1,44 @@
1
+ import { UriUtils } from "langium";
2
+ import { code, fence, rule, sections } from "../markdown/index.js";
3
+ import { decoratorDoc } from "./builtin-docs.js";
4
+ import type { DecoInfo } from "./deco.types.js";
5
+
6
+ /**
7
+ * Hover for a decorator: the same text whether the cursor sits on `@memoize`
8
+ * or on the `deco memoize` that defines it. They are one thing seen twice, so
9
+ * they read the same.
10
+ */
11
+ export function decoHover(info: DecoInfo): string {
12
+ return rule([fence(info.signature), sections([decorates(info), info.doc]), origin(info)]);
13
+ }
14
+
15
+ /**
16
+ * A decorator the language documents but no source declares, such as `@load`
17
+ * until `@venn-lang/load` contributes it. Explaining it beats saying nothing.
18
+ */
19
+ export function documentedHover(name: string): string | undefined {
20
+ const doc = decoratorDoc(name);
21
+ return doc && rule([fence(`@${name}`), doc]);
22
+ }
23
+
24
+ /** What it decorates, in one phrase: the detail beside a completion item. */
25
+ export function decoratesLabel(decorates: readonly string[]): string {
26
+ const named = namedKinds(decorates);
27
+ return named.length > 0 ? named.join(", ") : "anything";
28
+ }
29
+
30
+ function decorates(info: DecoInfo): string {
31
+ const named = namedKinds(info.decorates);
32
+ if (named.length === 0) return "Decorates **anything**.";
33
+ return `Decorates ${named.map(code).join(", ")}.`;
34
+ }
35
+
36
+ // `Node` is how a target says "anything", so it names nothing in particular.
37
+ function namedKinds(decorates: readonly string[]): string[] {
38
+ return decorates.filter((kind) => kind !== "Node");
39
+ }
40
+
41
+ function origin(info: DecoInfo): string {
42
+ if (!info.document) return "Built into the language.";
43
+ return `Declared in ${code(UriUtils.basename(info.document.uri))}`;
44
+ }
@@ -0,0 +1,93 @@
1
+ import { type AstNode, isAnnotation, isRef, isRunStmt, isValueImport } from "@venn-lang/core";
2
+ import { CstUtils, type LangiumDocument, type LangiumDocuments, type URI } from "langium";
3
+ import type { DefinitionProvider } from "langium/lsp";
4
+ import type { DefinitionParams, LocationLink } from "vscode-languageserver";
5
+ import { decoNamed } from "../deco/index.js";
6
+ import { findBinding, interpolationAt, resolveFragment } from "../document/index.js";
7
+ import type { VennServices } from "../services/lsp.types.js";
8
+ import type { ImportResolver } from "../workspace/index.js";
9
+
10
+ const ORIGIN = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
11
+
12
+ /** Ctrl+Click: `run` fragments (local or imported), bindings, and imported files. */
13
+ export class VennDefinitionProvider implements DefinitionProvider {
14
+ private readonly documents: LangiumDocuments;
15
+ private readonly imports: ImportResolver;
16
+
17
+ constructor(services: VennServices) {
18
+ this.documents = services.shared.workspace.LangiumDocuments;
19
+ this.imports = services.imports;
20
+ }
21
+
22
+ async getDefinition(
23
+ document: LangiumDocument,
24
+ params: DefinitionParams,
25
+ ): Promise<LocationLink[] | undefined> {
26
+ const root = document.parseResult?.value?.$cstNode;
27
+ if (!root) return undefined;
28
+ const offset = document.textDocument.offsetAt(params.position);
29
+ const inString = this.interpolated({ document, offset });
30
+ if (inString) return [inString];
31
+ const node = CstUtils.findLeafNodeAtOffset(root, offset)?.astNode;
32
+ const link = node && (await this.resolve(node, document));
33
+ return link ? [link] : undefined;
34
+ }
35
+
36
+ /** `"${base}/users"`: the name inside the placeholder is a real reference. */
37
+ private interpolated(args: {
38
+ document: LangiumDocument;
39
+ offset: number;
40
+ }): LocationLink | undefined {
41
+ const hit = interpolationAt(args);
42
+ if (!hit?.isHead) return undefined;
43
+ return localLink(findBinding(hit.host, hit.name), args.document);
44
+ }
45
+
46
+ private async resolve(
47
+ node: AstNode,
48
+ document: LangiumDocument,
49
+ ): Promise<LocationLink | undefined> {
50
+ if (isRunStmt(node)) return this.fragment(node.target, document);
51
+ if (isAnnotation(node)) return this.decorator(node.name, document);
52
+ if (isRef(node)) return localLink(findBinding(node, node.name), document);
53
+ if (isValueImport(node)) return fileLink(this.imports.resolve(node.path, document.uri));
54
+ return undefined;
55
+ }
56
+
57
+ // `@memoize` lands on the `deco memoize` that defines it, here or across an
58
+ // import. A built-in has no source to land on, so nothing happens.
59
+ private async decorator(
60
+ name: string,
61
+ document: LangiumDocument,
62
+ ): Promise<LocationLink | undefined> {
63
+ const scope = { document, documents: this.documents, imports: this.imports };
64
+ const found = await decoNamed(name, scope);
65
+ return found?.document ? localLink(found.decl, found.document) : undefined;
66
+ }
67
+
68
+ // Land on the declaration when the file can be read; otherwise open the file.
69
+ private async fragment(
70
+ name: string,
71
+ document: LangiumDocument,
72
+ ): Promise<LocationLink | undefined> {
73
+ const location = await resolveFragment({
74
+ name,
75
+ document,
76
+ documents: this.documents,
77
+ imports: this.imports,
78
+ });
79
+ if (!location) return undefined;
80
+ const precise = location.document && localLink(location.decl, location.document);
81
+ return precise ?? fileLink(location.uri);
82
+ }
83
+ }
84
+
85
+ function localLink(node: AstNode | undefined, document: LangiumDocument): LocationLink | undefined {
86
+ const range = node?.$cstNode?.range;
87
+ if (!range) return undefined;
88
+ return { targetUri: document.uri.toString(), targetRange: range, targetSelectionRange: range };
89
+ }
90
+
91
+ function fileLink(uri: URI): LocationLink {
92
+ return { targetUri: uri.toString(), targetRange: ORIGIN, targetSelectionRange: ORIGIN };
93
+ }
@@ -0,0 +1 @@
1
+ export { VennDefinitionProvider } from "./definition.js";
@@ -0,0 +1,17 @@
1
+ /** One documented parameter. */
2
+ export interface DocParam {
3
+ name: string;
4
+ text: string;
5
+ }
6
+
7
+ /**
8
+ * A parsed documentation block: the `##` lines above a declaration, or its
9
+ * `@doc("…")` annotation (§08). Summary and every field are markdown.
10
+ */
11
+ export interface DocBlock {
12
+ summary: string;
13
+ params: DocParam[];
14
+ returns?: string;
15
+ examples: string[];
16
+ deprecated?: string;
17
+ }
@@ -0,0 +1,4 @@
1
+ export type { DocBlock, DocParam } from "./doc.types.js";
2
+ export { parseDoc } from "./parse-doc.js";
3
+ export { readDoc } from "./read-doc.js";
4
+ export { renderDoc } from "./render-doc.js";
@@ -0,0 +1,42 @@
1
+ import type { DocBlock } from "./doc.types.js";
2
+
3
+ const TAG = /^@(param|returns?|example|deprecated)\b[ \t]*(.*)$/;
4
+
5
+ interface Section {
6
+ tag: string;
7
+ lines: string[];
8
+ }
9
+
10
+ /** Split doc lines into a summary plus `@param` / `@returns` / `@example` / `@deprecated`. */
11
+ export function parseDoc(lines: readonly string[]): DocBlock {
12
+ const doc: DocBlock = { summary: "", params: [], examples: [] };
13
+ const summary: string[] = [];
14
+ let section: Section | undefined;
15
+ for (const line of lines) {
16
+ const match = TAG.exec(line.trim());
17
+ if (!match) (section ? section.lines : summary).push(line);
18
+ else {
19
+ apply(doc, section);
20
+ section = { tag: match[1] as string, lines: [match[2] ?? ""] };
21
+ }
22
+ }
23
+ apply(doc, section);
24
+ doc.summary = summary.join("\n").trim();
25
+ return doc;
26
+ }
27
+
28
+ function apply(doc: DocBlock, section: Section | undefined): void {
29
+ if (!section) return;
30
+ const text = section.lines.join("\n").trim();
31
+ if (section.tag === "param") doc.params.push(splitParam(text));
32
+ else if (section.tag === "example") doc.examples.push(text);
33
+ else if (section.tag === "deprecated") doc.deprecated = text || "This is deprecated.";
34
+ else doc.returns = text;
35
+ }
36
+
37
+ // `@param user The account to log in with.`: the first word is the name.
38
+ function splitParam(text: string): { name: string; text: string } {
39
+ const space = text.search(/\s/);
40
+ if (space < 0) return { name: text, text: "" };
41
+ return { name: text.slice(0, space), text: text.slice(space).trim() };
42
+ }
@@ -0,0 +1,45 @@
1
+ import { type Annotation, type AstNode, isStringLit } from "@venn-lang/core";
2
+ import type { LangiumDocument } from "langium";
3
+ import type { DocBlock } from "./doc.types.js";
4
+ import { parseDoc } from "./parse-doc.js";
5
+
6
+ const DOC_PREFIX = "##";
7
+
8
+ /**
9
+ * The documentation attached to a declaration: a `##` block directly above it,
10
+ * or its `@doc("…")` annotation (§08). `##` wins when both are present.
11
+ *
12
+ * @returns The parsed block, or `undefined` when the declaration has neither.
13
+ */
14
+ export function readDoc(document: LangiumDocument, node: AstNode): DocBlock | undefined {
15
+ const lines = docLinesAbove(document, node);
16
+ if (lines.length > 0) return parseDoc(lines);
17
+ const annotated = docAnnotation(node);
18
+ return annotated ? parseDoc(annotated.split("\n")) : undefined;
19
+ }
20
+
21
+ // Walk back over whole lines while they keep starting with `##`.
22
+ function docLinesAbove(document: LangiumDocument, node: AstNode): string[] {
23
+ const offset = node.$cstNode?.offset;
24
+ if (offset === undefined) return [];
25
+ const before = document.textDocument.getText().slice(0, offset).split("\n");
26
+ before.pop();
27
+ const lines: string[] = [];
28
+ for (let index = before.length - 1; index >= 0; index--) {
29
+ const text = (before[index] ?? "").trim();
30
+ if (!text.startsWith(DOC_PREFIX)) break;
31
+ lines.unshift(strip(text));
32
+ }
33
+ return lines;
34
+ }
35
+
36
+ function strip(line: string): string {
37
+ return line.slice(DOC_PREFIX.length).replace(/^ /, "");
38
+ }
39
+
40
+ function docAnnotation(node: AstNode): string | undefined {
41
+ const annotations = (node as { annotations?: Annotation[] }).annotations ?? [];
42
+ const doc = annotations.find((annotation) => annotation.name === "doc");
43
+ const first = doc?.args?.args?.[0]?.value;
44
+ return first && isStringLit(first) ? first.value : undefined;
45
+ }
@@ -0,0 +1,42 @@
1
+ import { code, fence, labelled, rule, sections } from "../markdown/index.js";
2
+ import type { DocBlock, DocParam } from "./doc.types.js";
3
+
4
+ /**
5
+ * Render a doc block. The prose keeps its own paragraph rhythm; the tagged
6
+ * sections sit below a rule so they read as a distinct band, not as more prose.
7
+ *
8
+ * @returns Markdown, or `undefined` when the block has nothing worth showing.
9
+ */
10
+ export function renderDoc(doc: DocBlock | undefined): string | undefined {
11
+ if (!doc) return undefined;
12
+ const prose = sections([deprecatedLine(doc.deprecated), doc.summary || undefined]);
13
+ const tags = sections([
14
+ paramsSection(doc.params),
15
+ returnsLine(doc.returns),
16
+ examplesSection(doc.examples),
17
+ ]);
18
+ return rule([prose || undefined, tags || undefined]) || undefined;
19
+ }
20
+
21
+ function deprecatedLine(deprecated: string | undefined): string | undefined {
22
+ return deprecated && `⚠️ **Deprecated** — ${deprecated}`;
23
+ }
24
+
25
+ function paramsSection(params: readonly DocParam[]): string | undefined {
26
+ if (params.length === 0) return undefined;
27
+ const items = params.map((param) => `- ${code(param.name)}${suffix(param.text)}`);
28
+ return labelled("**Parameters**", items.join("\n"));
29
+ }
30
+
31
+ function suffix(text: string): string {
32
+ return text ? ` — ${text}` : "";
33
+ }
34
+
35
+ function returnsLine(returns: string | undefined): string | undefined {
36
+ return returns && `**Returns** — ${returns}`;
37
+ }
38
+
39
+ function examplesSection(examples: readonly string[]): string | undefined {
40
+ if (examples.length === 0) return undefined;
41
+ return labelled("**Example**", examples.map((example) => fence(example)).join("\n"));
42
+ }
@@ -0,0 +1,30 @@
1
+ import { type AstNode, type Document, isDecoDecl, isFnDecl, isFragmentDecl } from "@venn-lang/core";
2
+
3
+ /** A name a module publishes, and which kind of thing it is. */
4
+ export interface ExportedName {
5
+ name: string;
6
+ /** `fragment`, `fn` or `deco`: what the editor draws it as. */
7
+ origin: string;
8
+ }
9
+
10
+ /**
11
+ * The names a module marks `pub`: everything another file is allowed to import.
12
+ * A `deco` crosses files like a fragment or a function does.
13
+ *
14
+ * The kind travels with the name because the two are read together everywhere:
15
+ * an `import { … }` list draws a fragment apart from a function, and `run`
16
+ * accepts only one of them.
17
+ */
18
+ export function exportedNames(document: Document): ExportedName[] {
19
+ const found: ExportedName[] = [];
20
+ for (const decl of document.decls) {
21
+ if (!isFragmentDecl(decl) && !isFnDecl(decl) && !isDecoDecl(decl)) continue;
22
+ if (decl.export) found.push({ name: decl.name, origin: originOf(decl) });
23
+ }
24
+ return found;
25
+ }
26
+
27
+ function originOf(decl: AstNode): string {
28
+ if (isFragmentDecl(decl)) return "fragment";
29
+ return isFnDecl(decl) ? "fn" : "deco";
30
+ }
@@ -0,0 +1,43 @@
1
+ import { type AstNode, type Expr, isMember, isRef } from "@venn-lang/core";
2
+ import type { SymbolCatalog } from "../catalog/index.js";
3
+ import { findBinding } from "./find-binding.js";
4
+
5
+ /** The dotted path an expression spells, or undefined when it is not a plain one. */
6
+ export function pathOf(expr: Expr | AstNode | undefined): string | undefined {
7
+ if (!expr) return undefined;
8
+ if (isRef(expr)) return expr.name;
9
+ if (!isMember(expr) || expr.optional) return undefined;
10
+ const receiver = pathOf(expr.receiver);
11
+ return receiver === undefined ? undefined : `${receiver}.${expr.member}`;
12
+ }
13
+
14
+ /**
15
+ * The action a path names, if the catalog knows it. `let auth = http.post …`
16
+ * spells a call as a member chain; only the catalog can tell that apart from
17
+ * `res.status`, and both the colours and the hover need the same answer.
18
+ */
19
+ export function stdlibAction(
20
+ node: AstNode | undefined,
21
+ catalog: SymbolCatalog,
22
+ ): string | undefined {
23
+ const path = pathOf(outermost(node));
24
+ const dot = path?.indexOf(".") ?? -1;
25
+ if (!path || dot < 0 || !node) return undefined;
26
+ // A name in scope always wins, so a variable called `auth` stays a variable
27
+ // however many stdlib namespaces happen to share its name.
28
+ if (findBinding(node, path.slice(0, dot))) return undefined;
29
+ return catalog.action(path.slice(0, dot), path.slice(dot + 1)) ? path : undefined;
30
+ }
31
+
32
+ /** Climb to the end of the member chain: from `http` in `http.post`, to `http.post`. */
33
+ function outermost(node: AstNode | undefined): AstNode | undefined {
34
+ let current = node;
35
+ while (
36
+ current?.$container &&
37
+ isMember(current.$container) &&
38
+ current.$container.receiver === current
39
+ ) {
40
+ current = current.$container;
41
+ }
42
+ return current;
43
+ }
@@ -0,0 +1,82 @@
1
+ import {
2
+ type AstNode,
3
+ type Block,
4
+ type Document,
5
+ type FragmentDecl,
6
+ isBlock,
7
+ isCaptureStmt,
8
+ isDatasetDecl,
9
+ isDecoDecl,
10
+ isDocument,
11
+ isFnBody,
12
+ isFnDecl,
13
+ isFnExpr,
14
+ isForEachStmt,
15
+ isFragmentDecl,
16
+ isLetStmt,
17
+ isRepeatStmt,
18
+ type ParamList,
19
+ } from "@venn-lang/core";
20
+
21
+ /** The node that binds `name`, searched from `from` outwards to the document. */
22
+ export function findBinding(from: AstNode, name: string): AstNode | undefined {
23
+ let node: AstNode | undefined = from;
24
+ while (node) {
25
+ const found = bindingIn(node, name);
26
+ if (found) return found;
27
+ node = node.$container;
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ /** The fragment a document declares under `name`. */
33
+ export function findFragment(document: Document, name: string): FragmentDecl | undefined {
34
+ return document.decls.find(
35
+ (decl): decl is FragmentDecl => isFragmentDecl(decl) && decl.name === name,
36
+ );
37
+ }
38
+
39
+ /**
40
+ * The top-level declaration a document makes under `name`.
41
+ *
42
+ * The same lookup a name written in that file resolves to, reused from outside
43
+ * it, which is what following an `import` does.
44
+ */
45
+ export function findDeclaration(document: Document, name: string): AstNode | undefined {
46
+ return documentBinding(document, name);
47
+ }
48
+
49
+ function bindingIn(node: AstNode, name: string): AstNode | undefined {
50
+ if (isForEachStmt(node)) return node.item === name ? node : undefined;
51
+ if (isRepeatStmt(node)) return node.index === name ? node : undefined;
52
+ // A `deco` binds its parameters the way a `fn` does. The first one being the
53
+ // target is a fact about its type, not about how it comes into scope.
54
+ if (isFragmentDecl(node) || isFnDecl(node) || isFnExpr(node) || isDecoDecl(node))
55
+ return paramNamed(node.params, name);
56
+ if (isFnBody(node)) return node.locals.find((local) => local.name === name);
57
+ if (isBlock(node)) return statementBinding(node, name);
58
+ if (isDocument(node)) return documentBinding(node, name);
59
+ return undefined;
60
+ }
61
+
62
+ function statementBinding(block: Block, name: string): AstNode | undefined {
63
+ return block.stmts.find((stmt) => (isLetStmt(stmt) || isCaptureStmt(stmt)) && stmt.name === name);
64
+ }
65
+
66
+ function documentBinding(document: Document, name: string): AstNode | undefined {
67
+ return document.decls.find((decl) => declares(decl, name));
68
+ }
69
+
70
+ /**
71
+ * A `fn` is a value like any other: it can be passed, bound and called, so a
72
+ * file that declares one has bound that name.
73
+ */
74
+ function declares(decl: AstNode, name: string): boolean {
75
+ if (isLetStmt(decl) || isDatasetDecl(decl)) return decl.name === name;
76
+ if (isFnDecl(decl) || isDecoDecl(decl)) return decl.name === name;
77
+ return isFragmentDecl(decl) && decl.name === name;
78
+ }
79
+
80
+ function paramNamed(params: ParamList | undefined, name: string): AstNode | undefined {
81
+ return (params?.params ?? []).find((param) => param.name === name);
82
+ }