@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,168 @@
1
+ import { fence, rule, sections } from "../markdown/index.js";
2
+
3
+ interface KeywordDoc {
4
+ summary: string;
5
+ example?: string;
6
+ }
7
+
8
+ // What every word of the kernel does. The grammar is fixed and small, so this
9
+ // table is the whole language: a newcomer can learn it by hovering.
10
+ const KEYWORDS: Record<string, KeywordDoc> = {
11
+ module: {
12
+ summary: "Name this file. Purely documentation; imports resolve by path, not by module name.",
13
+ example: "module checkout.payment",
14
+ },
15
+ use: {
16
+ summary: "Load a plugin package, making its namespace available as verbs.",
17
+ example: 'use "venn/http"\nuse "venn/browser" as b',
18
+ },
19
+ import: {
20
+ summary: "Bring `pub` fragments and functions from another `.vn` file into this one.",
21
+ example: 'import { login } from "./shared/auth.vn"',
22
+ },
23
+ from: {
24
+ summary: "The module an `import` reads from: a relative path or a `#alias` from `venn.toml`.",
25
+ },
26
+ as: {
27
+ summary: "Rename what you just bound — a `use` namespace, a `repeat` index or a `run` result.",
28
+ },
29
+ pub: {
30
+ summary: "Export this declaration so other files may `import` it.",
31
+ example: "pub fragment login(user) { … }",
32
+ },
33
+ flow: {
34
+ summary: "A test: the top-level unit the runner schedules and reports.",
35
+ example:
36
+ 'flow "Checkout" {\n step "Pay" {\n const paid = http.post "/pay"\n expect paid.status == 200\n }\n}',
37
+ },
38
+ step: {
39
+ summary: "One named unit of work inside a flow. Bindings made inside it are step-local.",
40
+ example: 'step "Ping" {\n const health = http.get "/health"\n expect health.status == 200\n}',
41
+ },
42
+ group: {
43
+ summary: "Label a set of steps. Structure only — it changes no scope.",
44
+ example: 'group "payment" {\n step "Charge" { … }\n}',
45
+ },
46
+ fragment: {
47
+ summary: "A reusable block of steps, invoked with `run`. Composition without inheritance.",
48
+ example: 'fragment login(user) {\n step "login" { … }\n}',
49
+ },
50
+ fn: {
51
+ summary:
52
+ "A pure function returning a value — first-class, callable anywhere. `=> expr` for one line; a `{ … }` block returns its last expression. No steps, no I/O.",
53
+ example: "fn double(x) => x * 2\nconst add = fn (a, b) => a + b",
54
+ },
55
+ deco: {
56
+ summary:
57
+ "Declare a decorator. The first parameter is the target, and the type written on it is what `@name` may sit on — `Fn`, `Flow`, `Step`, `Binding`, `Type`, `Resource`, or `Node` for anything. The parameters after it are the decorator's own arguments. The body runs before the program exists, so it is pure: no plugin verbs.",
58
+ example:
59
+ 'deco memoize(target: Fn) {\n const cache = {}\n target.wrap(fn (call, args) => cache.get(str(args)) ?? call(args))\n}\n\npub deco retry(target: Flow, times: number) {\n target.meta "retry" times\n}',
60
+ },
61
+ run: {
62
+ summary: "Invoke a fragment, optionally binding what it returns.",
63
+ example: 'run login("alice") as session',
64
+ },
65
+ expect: {
66
+ summary:
67
+ "Assert. Takes a boolean, or a subject plus a matcher. `.all { … }` groups checks; `.soft` records without aborting.",
68
+ example: 'expect health.status == 200\nexpect user.plan oneOf ["free", "pro"]',
69
+ },
70
+ not: { summary: "Negate the expectation that follows.", example: "expect not orders empty" },
71
+ all: { summary: "Group several checks under one `expect`, each on its own line." },
72
+ soft: { summary: "Record a failed expectation without aborting the step." },
73
+ let: { summary: "Bind a value for the rest of the block.", example: 'let plan = "pro"' },
74
+ const: { summary: "Bind a value that cannot be reassigned.", example: "const retries = 3" },
75
+ capture: {
76
+ summary:
77
+ "Removed. It did exactly what `let` does — use `let` for a value that changes, `const` for one that does not.",
78
+ example: "const orderId = order.json.id",
79
+ },
80
+ env: {
81
+ summary:
82
+ "Configuration for this run, read from the `[env.*]` tables of `venn.toml`. `--env <name>` picks which table; `env.name` is the one that was picked.",
83
+ example:
84
+ 'use "venn/env"\n\nconst token = http.post "${env.BASE_URL}/token" {\n body: { user: env.USERNAME, pass: env.PASSWORD },\n encode: "form"\n}',
85
+ },
86
+ resource: {
87
+ summary:
88
+ "Open something with a lifecycle, closed automatically. `@scope` sets how long it lives.",
89
+ example: "@scope(flow)\nresource page = browser.newContext",
90
+ },
91
+ config: {
92
+ summary: "Project settings visible to plugins, such as `baseUrl`.",
93
+ example: "config { baseUrl: env.BASE_URL }",
94
+ },
95
+ matrix: {
96
+ summary: "Run every flow once per combination of these values.",
97
+ example: 'matrix { browser: ["chromium", "webkit"] }',
98
+ },
99
+ dataset: { summary: "A named collection of test data." },
100
+ factory: { summary: "A named builder for objects, usually backed by `@venn-lang/data`." },
101
+ type: {
102
+ summary: "A named shape, used to type parameters and datasets.",
103
+ example: "type User { email: string }",
104
+ },
105
+ report: { summary: "Where results go.", example: 'report junit("./out")' },
106
+ if: {
107
+ summary: "Branch on a condition.",
108
+ example: 'if health.status == 200 {\n step "ok" { … }\n} else {\n step "retry" { … }\n}',
109
+ },
110
+ else: { summary: "The branch taken when the `if` condition is false." },
111
+ forEach: {
112
+ summary: "Repeat the body once per item. The optional map sets `concurrency`.",
113
+ example: 'forEach user in users { concurrency: 4 } {\n step "check" { … }\n}',
114
+ },
115
+ in: {
116
+ summary: "Separates the loop variable from its list, and tests membership in an expression.",
117
+ },
118
+ repeat: {
119
+ summary: "Run the body a fixed number of times, optionally binding the 1-based index.",
120
+ example: 'repeat 3 as attempt {\n step "poll" { … }\n}',
121
+ },
122
+ while: { summary: "Repeat while a condition holds. Every loop inherits a timeout." },
123
+ parallel: {
124
+ summary: "Run the statements concurrently and wait for all of them.",
125
+ example: 'parallel { concurrency: 4 } {\n step "a" { … }\n step "b" { … }\n}',
126
+ },
127
+ race: {
128
+ summary: "Run concurrently; the first to finish wins and the others are cancelled.",
129
+ example: 'race { timeout: 10s } {\n step "ws" { … }\n step "poll" { … }\n}',
130
+ },
131
+ try: { summary: "Attempt the body, handling failure in `catch` and cleanup in `finally`." },
132
+ catch: { summary: "Handle a failure from `try`, optionally binding the error." },
133
+ finally: { summary: "Always runs after `try`, whether it failed or not." },
134
+ defer: {
135
+ summary: "Schedule cleanup that runs on the way out of the block, even on failure.",
136
+ example: 'defer { db.exec "ROLLBACK" }',
137
+ },
138
+ setup: { summary: "Runs once before every flow in the file." },
139
+ teardown: { summary: "Runs once after every flow in the file." },
140
+ beforeEach: { summary: "Runs before each flow." },
141
+ afterEach: { summary: "Runs after each flow." },
142
+ on: {
143
+ summary: "React to a lifecycle event.",
144
+ example: "on failure {\n artifacts.save trace\n}",
145
+ },
146
+ return: {
147
+ summary:
148
+ "Return a value early. A `fn` already returns its last expression, so `return` is only needed to leave sooner; it also stops a fragment.",
149
+ },
150
+ break: { summary: "Leave the innermost loop." },
151
+ continue: { summary: "Skip to the next iteration of the innermost loop." },
152
+ true: { summary: "The boolean true." },
153
+ false: { summary: "The boolean false." },
154
+ null: { summary: "The absence of a value." },
155
+ };
156
+
157
+ /** Hover for a keyword of the kernel: what it does, and how it reads. */
158
+ export function keywordHover(word: string): string | undefined {
159
+ const doc = KEYWORDS[word];
160
+ if (!doc) return undefined;
161
+ const example = doc.example ? sections(["**Example**", fence(doc.example)]) : undefined;
162
+ return rule([fence(word), sections([doc.summary, example])]);
163
+ }
164
+
165
+ /** Whether the kernel documents this word at all. */
166
+ export function isKeyword(word: string): boolean {
167
+ return word in KEYWORDS;
168
+ }
@@ -0,0 +1,94 @@
1
+ import { type ParamSpec, paramSpecs } from "@venn-lang/sdk";
2
+ import type { ActionEntry, MatcherEntry, SymbolCatalog } from "../catalog/index.js";
3
+ import { code, fence, labelled, rule, sections } from "../markdown/index.js";
4
+ import { callShape, type ShownArg, shownArgs } from "../signature/index.js";
5
+ import { preludeHover } from "./render-symbol.js";
6
+
7
+ /** Hover for `namespace.action`, or for a prelude verb like `log`. */
8
+ export function actionHover(target: string, catalog: SymbolCatalog): string | undefined {
9
+ const dot = target.indexOf(".");
10
+ if (dot < 0) return preludeHover(target);
11
+ const entry = catalog.action(target.slice(0, dot), target.slice(dot + 1));
12
+ return entry && renderAction(entry, catalog);
13
+ }
14
+
15
+ /** Hover for a bareword matcher used after `expect`. */
16
+ export function matcherHover(name: string, catalog: SymbolCatalog): string | undefined {
17
+ const entry = catalog.matcher(name);
18
+ return entry && renderMatcher(entry);
19
+ }
20
+
21
+ function renderAction(entry: ActionEntry, catalog: SymbolCatalog): string {
22
+ const target = `${entry.namespace}.${entry.name}`;
23
+ const shape = callShape(target, catalog);
24
+ const returns = shape?.returns ? ` -> ${shape.returns}` : "";
25
+ const body = sections([
26
+ entry.action.doc,
27
+ argumentsBlock(shape?.args ?? []),
28
+ optionsBlock(paramSpecs(entry.action.params)),
29
+ ]);
30
+ return rule([
31
+ fence(`${signatureLine(target, shape?.args ?? [])}${returns}`),
32
+ body || undefined,
33
+ WHAT_IT_IS,
34
+ `**Package** ${code(entry.package)}`,
35
+ ]);
36
+ }
37
+
38
+ /**
39
+ * What kind of thing this is, said outright.
40
+ *
41
+ * A `fn` hover carries the word `fn` and needs no explaining. A verb carries
42
+ * nothing that tells it apart from a function the reader could hold, which is
43
+ * exactly what it is not.
44
+ */
45
+ const WHAT_IT_IS =
46
+ "**Verb** — a package contributes it, it reaches the world outside, and the program waits for it without saying so. Naming it calls it; it is not a value you can hold.";
47
+
48
+ /** The call as it is written: `http.on server handler`, with no brackets. */
49
+ function signatureLine(target: string, args: readonly ShownArg[]): string {
50
+ const written = args.map((each) => (each.optional ? `${each.name}?` : each.name)).filter(Boolean);
51
+ return [target, ...written].join(" ");
52
+ }
53
+
54
+ /** One line per positional argument: the half a Zod schema never describes. */
55
+ function argumentsBlock(args: readonly ShownArg[]): string | undefined {
56
+ const named = args.filter((each) => each.name);
57
+ if (named.length === 0) return undefined;
58
+ return labelled("Arguments", named.map(argumentLine).join("\n"));
59
+ }
60
+
61
+ function argumentLine(arg: ShownArg): string {
62
+ const optional = arg.optional ? " *(optional)*" : "";
63
+ const doc = arg.doc ? ` — ${arg.doc}` : "";
64
+ return `- ${code(arg.name)}: ${code(arg.type)}${optional}${doc}`;
65
+ }
66
+
67
+ function renderMatcher(entry: MatcherEntry): string {
68
+ const applies = entry.matcher.appliesTo
69
+ ? `Applies to ${code(entry.matcher.appliesTo)}.`
70
+ : undefined;
71
+ const args = shownArgs(entry.matcher.args ?? []);
72
+ const body = sections([
73
+ applies,
74
+ argumentsBlock(args),
75
+ optionsBlock(paramSpecs(entry.matcher.params)),
76
+ ]);
77
+ return rule([
78
+ fence(signatureLine(`expect <subject> ${entry.name}`, args)),
79
+ body || undefined,
80
+ `**Package** ${code(entry.package)}`,
81
+ ]);
82
+ }
83
+
84
+ /** One line per key: what to write, whether it is required, and what it means. */
85
+ function optionsBlock(specs: ParamSpec[]): string | undefined {
86
+ if (specs.length === 0) return undefined;
87
+ return labelled("Options", specs.map(optionLine).join("\n"));
88
+ }
89
+
90
+ function optionLine(spec: ParamSpec): string {
91
+ const required = spec.required ? " *(required)*" : "";
92
+ const doc = spec.doc ? ` — ${spec.doc}` : "";
93
+ return `- ${code(spec.name)}: ${code(spec.type)}${required}${doc}`;
94
+ }
@@ -0,0 +1,155 @@
1
+ import {
2
+ type AstNode,
3
+ type FnDecl,
4
+ type FragmentDecl,
5
+ isCaptureStmt,
6
+ isDatasetDecl,
7
+ isDecoDecl,
8
+ isFnDecl,
9
+ isFnExpr,
10
+ isForEachStmt,
11
+ isFragmentDecl,
12
+ isLetStmt,
13
+ isParam,
14
+ isRepeatStmt,
15
+ type ParamList,
16
+ type UseDecl,
17
+ } from "@venn-lang/core";
18
+ import { type LangiumDocument, UriUtils } from "langium";
19
+ import type { SymbolCatalog } from "../catalog/index.js";
20
+ import { readDoc, renderDoc } from "../docs/index.js";
21
+ import { type FragmentLocation, findBinding } from "../document/index.js";
22
+ import { code, fence, rule, sections } from "../markdown/index.js";
23
+ import type { TypeService } from "../types/index.js";
24
+ import { typeLabel } from "./type-hover.js";
25
+
26
+ /** Hover for a fragment: its signature, its documentation, and where it lives. */
27
+ export function fragmentHover(location: FragmentLocation): string {
28
+ const file = code(UriUtils.basename(location.uri));
29
+ if (!location.decl || !location.document) {
30
+ return rule([fence("fragment …"), `Imported from ${file}.`]);
31
+ }
32
+ const { decl, document } = location;
33
+ const params = (decl.params?.params ?? []).map((param) => param.name).join(", ");
34
+ const signature = `${decl.export ? "pub " : ""}fragment ${decl.name}(${params})`;
35
+ return rule([fence(signature), renderDoc(readDoc(document, decl)), `Declared in ${file}`]);
36
+ }
37
+
38
+ /** Hover for a variable reference, resolved to whatever binds it. */
39
+ export function bindingHover(args: {
40
+ document: LangiumDocument;
41
+ node: AstNode;
42
+ name: string;
43
+ types: TypeService;
44
+ waiting?: ReadonlySet<string>;
45
+ }): string | undefined {
46
+ const binding = findBinding(args.node, args.name);
47
+ return binding && renderBinding({ ...args, binding });
48
+ }
49
+
50
+ /** Hover on the declaration itself (`let plan = …`, `resource db = …`). */
51
+ export function declarationHover(args: {
52
+ document: LangiumDocument;
53
+ node: AstNode;
54
+ types: TypeService;
55
+ /** Names of the functions in this file that wait for something. */
56
+ waiting?: ReadonlySet<string>;
57
+ }): string | undefined {
58
+ const name = declaredName(args.node);
59
+ if (name === undefined) return undefined;
60
+ return renderBinding({ ...args, binding: args.node, name });
61
+ }
62
+
63
+ /** Hover for `use "venn/http"`: what the package contributes. */
64
+ export function useHover(decl: UseDecl, catalog: SymbolCatalog): string | undefined {
65
+ const namespace = catalog.namespaceOfPackage(decl.pkg);
66
+ if (!namespace) return rule([fence(`use "${decl.pkg}"`), "Package is not loaded."]);
67
+ const count = catalog.actionsIn(namespace).length;
68
+ const alias = decl.alias ? ` as ${decl.alias}` : "";
69
+ const verbs = `Contributes ${count} action${count === 1 ? "" : "s"} under ${code(namespace)}.`;
70
+ return rule([fence(`use "${decl.pkg}"${alias}`), verbs]);
71
+ }
72
+
73
+ function renderBinding(args: {
74
+ document: LangiumDocument;
75
+ binding: AstNode;
76
+ name: string;
77
+ types: TypeService;
78
+ waiting?: ReadonlySet<string>;
79
+ }): string {
80
+ const { document, binding, name } = args;
81
+ const waits = Boolean(args.waiting?.has(name)) && isFnDecl(binding);
82
+ const body = sections([renderDoc(readDoc(document, binding)), describe(binding), waited(waits)]);
83
+ return rule([fence(signatureOf(binding, name, args.types, waits)), body || undefined]);
84
+ }
85
+
86
+ /**
87
+ * Said plainly, because nothing in the source says it. The runtime waits for
88
+ * this on its own, with no keyword written, so the editor is the only place a
89
+ * reader can find out.
90
+ */
91
+ function waited(waits: boolean): string | undefined {
92
+ if (!waits) return undefined;
93
+ return "**Waits** — it reaches for a plugin verb, so it hands back a value that is still arriving. Anything that binds it with `let` gets the value.";
94
+ }
95
+
96
+ function declaredName(node: AstNode): string | undefined {
97
+ if (isLetStmt(node) || isCaptureStmt(node)) return node.name;
98
+ if (isFnDecl(node)) return node.name;
99
+ if (isDatasetDecl(node)) return node.name;
100
+ return isParam(node) ? node.name : undefined;
101
+ }
102
+
103
+ /** The signature line, carrying the inferred type when the checker knows one. */
104
+ function signatureOf(binding: AstNode, name: string, types: TypeService, waits = false): string {
105
+ // `~>` rather than `->`: the arrow says the answer takes a moment.
106
+ const type = waiting(typeLabel(binding, types), waits);
107
+ const typed = type ? `: ${type}` : "";
108
+ if (isFnDecl(binding))
109
+ return `${binding.export ? "pub " : ""}fn ${written(binding, name)}${typed}`;
110
+ if (isLetStmt(binding)) return `${binding.kind} ${name}${typed}`;
111
+ if (isCaptureStmt(binding)) return `capture ${name}`;
112
+ if (isDatasetDecl(binding)) return `dataset ${name}`;
113
+ if (isFragmentDecl(binding)) return `fragment ${written(binding, name)}`;
114
+ if (isParam(binding)) return `${name}${typed || ": parameter"}`;
115
+ return name;
116
+ }
117
+
118
+ /**
119
+ * The name with the parameter list the source actually wrote.
120
+ *
121
+ * The type alone says `fn(a, b) -> a`, which tells the reader how many and not
122
+ * which. The names are right there in the declaration, and they are the half a
123
+ * caller has to get right.
124
+ */
125
+ function written(binding: FnDecl | FragmentDecl, name: string): string {
126
+ const names = (binding.params?.params ?? []).map((param) => param.name);
127
+ return `${name}(${names.join(", ")})`;
128
+ }
129
+
130
+ function waiting(type: string | undefined, waits: boolean): string | undefined {
131
+ return type && waits ? type.replace(" -> ", " ~> ") : type;
132
+ }
133
+
134
+ /** Where a parameter came from: a function's own list, a decorator's, a fragment's. */
135
+ function paramOwner(param: AstNode): string {
136
+ const owner = param.$container?.$container;
137
+ if (isFnDecl(owner)) return "Parameter of this `fn`.";
138
+ if (isFnExpr(owner)) return "Parameter of this function — typed by where it is called.";
139
+ if (isDecoDecl(owner)) return decoParam(param);
140
+ return "Fragment parameter.";
141
+ }
142
+
143
+ /** A `deco`'s first parameter is the thing decorated; the rest are its arguments. */
144
+ function decoParam(param: AstNode): string {
145
+ const list = param.$container as ParamList | undefined;
146
+ if (list?.params[0] !== param) return "Argument of this decorator, filled by `@name(…)`.";
147
+ return "What this decorator decorates — its type is the kind it may be written on.";
148
+ }
149
+
150
+ function describe(binding: AstNode): string | undefined {
151
+ if (isForEachStmt(binding)) return "Loop variable of `forEach`.";
152
+ if (isRepeatStmt(binding)) return "Loop index of `repeat`.";
153
+ if (isParam(binding)) return paramOwner(binding);
154
+ return undefined;
155
+ }
@@ -0,0 +1,49 @@
1
+ import { isDecoDecl, isFragmentDecl } from "@venn-lang/core";
2
+ import { UriUtils } from "langium";
3
+ import { declaredDeco, decoHover } from "../deco/index.js";
4
+ import type { ImportedLocation } from "../document/index.js";
5
+ import { code, fence, rule } from "../markdown/index.js";
6
+ import type { TypeService } from "../types/index.js";
7
+ import { declarationHover, fragmentHover } from "./render-decl.js";
8
+
9
+ export interface ImportedHoverArgs {
10
+ location: ImportedLocation;
11
+ name: string;
12
+ types: TypeService;
13
+ }
14
+
15
+ /**
16
+ * A name as it reads inside `import { … }`.
17
+ *
18
+ * Whatever the other file declared, described the way that file would describe
19
+ * it, so importing a name and standing on its declaration read alike. The
20
+ * import list is the one place a name is neither a declaration nor a use, so
21
+ * nothing else in the hover reaches it.
22
+ */
23
+ export function importedHover(args: ImportedHoverArgs): string | undefined {
24
+ const { location, name } = args;
25
+ const { decl, document } = location;
26
+ if (!decl || !document) return unread(location, name);
27
+ if (isFragmentDecl(decl)) return fragmentHover({ ...location, decl, document });
28
+ if (isDecoDecl(decl)) return decoHover(declaredDeco({ decl, document }));
29
+ return declarationHover({ document, node: decl, types: args.types });
30
+ }
31
+
32
+ /** The file could not be read, or declares no such name; say what is known. */
33
+ function unread(location: ImportedLocation, name: string): string {
34
+ return rule([fence(name), `Imported from ${code(UriUtils.basename(location.uri))}.`]);
35
+ }
36
+
37
+ /**
38
+ * The specifier itself: where `"#shared/auth.vn"` actually lands.
39
+ *
40
+ * An alias is the one specifier a reader cannot resolve in their head: it goes
41
+ * through `[paths]` in `venn.toml`, which is a different file. Saying the
42
+ * resolved path is the whole answer.
43
+ */
44
+ export function importPathHover(args: { location: ImportedLocation; path: string }): string {
45
+ const { location, path } = args;
46
+ const file = code(UriUtils.basename(location.uri));
47
+ const read = location.document ? `Resolves to ${file}.` : `Resolves to ${file} — not readable.`;
48
+ return rule([fence(`from "${path}"`), read]);
49
+ }
@@ -0,0 +1,108 @@
1
+ import {
2
+ createContext,
3
+ MEMBER_DOCS,
4
+ memberKind,
5
+ memberType,
6
+ PRELUDE_SPECS,
7
+ type PreludeArg,
8
+ resolveMember,
9
+ showType,
10
+ type Type,
11
+ } from "@venn-lang/core";
12
+ import type { SymbolCatalog } from "../catalog/index.js";
13
+ import { code, fence, labelled, rule, sections } from "../markdown/index.js";
14
+
15
+ /**
16
+ * Hover for a prelude name: `range`, `str`, `print`. They belong to no
17
+ * namespace and are declared nowhere, so without this they read as an unknown
18
+ * variable of type `dynamic`.
19
+ */
20
+ export function preludeHover(name: string): string | undefined {
21
+ const spec = PRELUDE_SPECS[name];
22
+ if (!spec) return undefined;
23
+ return rule([
24
+ fence(spec.signature),
25
+ sections([spec.doc, argumentsBlock(spec.args), example(spec.example)]),
26
+ "**Prelude** — available without `use`.",
27
+ ]);
28
+ }
29
+
30
+ /** One line per argument, laid out as an action's is; a verb is a verb. */
31
+ function argumentsBlock(args: readonly PreludeArg[] | undefined): string | undefined {
32
+ if (!args?.length) return undefined;
33
+ const lines = args.map((arg) => {
34
+ const doc = arg.doc ? ` — ${arg.doc}` : "";
35
+ return `- ${code(arg.name)}: ${code(arg.type)}${arg.optional ? " *(optional)*" : ""}${doc}`;
36
+ });
37
+ return labelled("Arguments", lines.join("\n"));
38
+ }
39
+
40
+ /**
41
+ * Hover for a namespace itself (`fmt`, `http`). A namespace is not a map: it is
42
+ * the set of verbs a package contributes, and saying so keeps anyone from
43
+ * reading its members like data.
44
+ */
45
+ export function namespaceHover(name: string, catalog: SymbolCatalog): string | undefined {
46
+ if (!catalog.hasNamespace(name)) return undefined;
47
+ const actions = catalog.actionsIn(name);
48
+ const shown = actions.slice(0, 8).map((entry) => code(`${name}.${entry.name}`));
49
+ const more = actions.length > shown.length ? `, …${actions.length - shown.length} more` : "";
50
+ return rule([
51
+ fence(`namespace ${name}`),
52
+ sections([
53
+ `Contributes ${actions.length} verb${actions.length === 1 ? "" : "s"}.`,
54
+ actions.length > 0 ? labelled("Verbs", `${shown.join(", ")}${more}`) : undefined,
55
+ ]),
56
+ `**Package** ${catalog.packagesFor(name).map(code).join(", ")}`,
57
+ ]);
58
+ }
59
+
60
+ /**
61
+ * Hover for a built-in member of a native value: `xs.map`, `name.slugify`.
62
+ * The signature comes from the receiver's inferred type, so `xs.first` on a
63
+ * `list<number>` reads as `number`, not as a type variable.
64
+ */
65
+ export function memberHover(args: { receiver: Type; member: string }): string | undefined {
66
+ const kind = memberKind(args.receiver);
67
+ const doc = kind && MEMBER_DOCS[kind]?.[args.member];
68
+ if (!doc) return fieldHover(args);
69
+ if (!kind) return undefined;
70
+ const type = memberType(args.receiver, args.member, createContext());
71
+ const shown = type ? ` -> ${showType(type)}` : "";
72
+ return rule([
73
+ fence(`${kind}.${args.member}${shown}`),
74
+ sections([doc.doc, example(doc.example)]),
75
+ `**Built in** — on every ${code(kind)}.`,
76
+ ]);
77
+ }
78
+
79
+ /** A key the data itself carries: `p.age` on `{ name: string, age: number }`. */
80
+ function fieldHover(args: { receiver: Type; member: string }): string | undefined {
81
+ const type = resolveMember(args.receiver, args.member, createContext());
82
+ if (!type) return undefined;
83
+ return rule([
84
+ fence(`${args.member}: ${showType(type)}`),
85
+ `**Field** of ${code(showType(args.receiver))}.`,
86
+ shaping(type),
87
+ ]);
88
+ }
89
+
90
+ /**
91
+ * How to get somewhere from a value nothing can know the shape of.
92
+ *
93
+ * `dynamic` is honest: a parsed response is whatever the far end sent, and no
94
+ * checker can say more. Honesty without a way forward reads as a dead end, so
95
+ * the hover spells out the way: name a type and annotate the binding.
96
+ */
97
+ function shaping(type: Type): string | undefined {
98
+ if (type.kind !== "dynamic") return undefined;
99
+ return [
100
+ "**Shape it by naming one** — nothing can know what this holds, so say what you expect:",
101
+ fence("type Price { symbol: string, price: number }\nconst price: Price = res.json"),
102
+ "From there it reads as a `Price`: members are offered, and a wrong one is an error.",
103
+ ].join("\n\n");
104
+ }
105
+
106
+ function example(source: string | undefined): string | undefined {
107
+ return source ? labelled("Example", fence(source)) : undefined;
108
+ }
@@ -0,0 +1,78 @@
1
+ import { BUILTIN_TYPES, KIND_SPECS, TARGET_KINDS } from "@venn-lang/core";
2
+ import type { RecordSpec, TypeSpec } from "@venn-lang/types";
3
+ import { showSpec } from "@venn-lang/types";
4
+ import type { SymbolCatalog } from "../catalog/index.js";
5
+ import { code, fence, labelled, rule, sections } from "../markdown/index.js";
6
+
7
+ /** What each decorator handle is a handle *on*, in the user's words. */
8
+ const DECORATES: Readonly<Record<string, string>> = {
9
+ Fn: "a `fn` — its parameters, and what happens around a call to it",
10
+ Flow: "a `flow` — its title, and what happens around its body",
11
+ Step: "a `step` — its title, and what happens around its body",
12
+ Binding: "a `let` or `const` — the value it holds",
13
+ Type: "a `type` — the fields it declares",
14
+ Resource: "a `resource` — its name and its metadata",
15
+ Node: "any declaration at all, by name",
16
+ };
17
+
18
+ /**
19
+ * Hover for a type written in an annotation: `target: Fn`, `x: string`.
20
+ *
21
+ * A type name is where the reader most needs telling. Unanswered, `Fn` looks
22
+ * like a name someone made up rather than the surface the body is about to use.
23
+ */
24
+ export function typeNameHover(name: string, catalog: SymbolCatalog): string | undefined {
25
+ return builtinHover(name) ?? kindHover(name) ?? publishedHover(name, catalog);
26
+ }
27
+
28
+ /** One of the language's own types: `string`, `duration`, `dynamic`. */
29
+ function builtinHover(name: string): string | undefined {
30
+ const builtin = BUILTIN_TYPES[name];
31
+ if (!builtin) return undefined;
32
+ return rule([
33
+ fence(name),
34
+ sections([builtin.doc, labelled("Written", fence(builtin.example))]),
35
+ "**Built in** — part of the language, no `use` needed.",
36
+ ]);
37
+ }
38
+
39
+ /** One of the seven decorator handles. */
40
+ function kindHover(name: string): string | undefined {
41
+ if (!TARGET_KINDS.includes(name as (typeof TARGET_KINDS)[number])) return undefined;
42
+ const spec = KIND_SPECS[name as keyof typeof KIND_SPECS];
43
+ return rule([
44
+ fence(`${name} — a decorator target`),
45
+ sections([
46
+ `Written as a \`deco\`'s first parameter, it decorates ${DECORATES[name] ?? "a declaration"}.`,
47
+ membersBlock(spec),
48
+ ]),
49
+ "The body runs at expansion time, before the program exists.",
50
+ ]);
51
+ }
52
+
53
+ /** A type a plugin published: `http.Response`, `db.Row`. */
54
+ function publishedHover(name: string, catalog: SymbolCatalog): string | undefined {
55
+ const dot = name.indexOf(".");
56
+ if (dot < 0) return undefined;
57
+ const found = catalog
58
+ .typesIn(name.slice(0, dot))
59
+ .find((entry) => entry.name === name.slice(dot + 1));
60
+ if (!found) return undefined;
61
+ return rule([
62
+ fence(`${found.namespace}.${found.name}`),
63
+ membersBlock(found.spec),
64
+ `**Package** ${code(found.package)}`,
65
+ ]);
66
+ }
67
+
68
+ /**
69
+ * One line per member, rather than one line for all of them. A handle carries
70
+ * ten verbs, and rendered end to end they run past the edge of any hover.
71
+ */
72
+ function membersBlock(spec: TypeSpec): string | undefined {
73
+ if (spec.kind !== "record") return undefined;
74
+ const fields = Object.entries((spec as RecordSpec).fields);
75
+ if (fields.length === 0) return undefined;
76
+ const lines = fields.map(([field, type]) => `- ${code(field)}: ${code(showSpec(type))}`);
77
+ return labelled("Offers", lines.join("\n"));
78
+ }