@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,61 @@
1
+ import { type AstNode, EXPRESSION_OFFSET } from "@venn-lang/core";
2
+ import { type CstNode, CstUtils, type LangiumDocument, type LeafCstNode } from "langium";
3
+ import { slotAt } from "./interpolation-at.js";
4
+
5
+ /** The slots a document's inference recorded, keyed by string literal. */
6
+ export interface SlotSource {
7
+ slots: ReadonlyMap<AstNode, readonly (AstNode | undefined)[]>;
8
+ }
9
+
10
+ /**
11
+ * The node a name at this offset should be resolved against.
12
+ *
13
+ * Usually that is simply the node under the cursor. Inside `"${…}"` it is not:
14
+ * the document's tree stops at the string, so a lambda parameter written in
15
+ * there exists only in the expression inference parsed. This reaches that one,
16
+ * which is what makes `p` in `"${xs.map(fn (p) => p.name)}"` resolve at all.
17
+ */
18
+ export function hostAt(args: HostArgs): AstNode | undefined {
19
+ return inInterpolation(args) ?? nodeAt(args);
20
+ }
21
+
22
+ export interface HostArgs {
23
+ document: LangiumDocument;
24
+ offset: number;
25
+ types: SlotSource;
26
+ /**
27
+ * Which token answers. Hover asks about the one under the cursor; completion
28
+ * about the one that *ends* at it, since `p.` is typed before it is asked.
29
+ */
30
+ prefer?: "at" | "before";
31
+ }
32
+
33
+ function inInterpolation(args: HostArgs): AstNode | undefined {
34
+ const hit = slotAt(args);
35
+ if (!hit) return undefined;
36
+ const root = args.types.slots.get(hit.host)?.[hit.index];
37
+ const cst = root?.$cstNode;
38
+ return cst && leafNear(cst, hit.inner + EXPRESSION_OFFSET, args.prefer)?.astNode;
39
+ }
40
+
41
+ function nodeAt(args: HostArgs): AstNode | undefined {
42
+ const root = args.document.parseResult?.value?.$cstNode;
43
+ return root ? leafNear(root, args.offset, args.prefer)?.astNode : undefined;
44
+ }
45
+
46
+ /**
47
+ * Mid-typing, the cursor often sits past the last token the parser kept (`p.`
48
+ * at the end of a line), so fall back to the token just before it.
49
+ */
50
+ function leafNear(
51
+ root: CstNode,
52
+ offset: number,
53
+ prefer?: "at" | "before",
54
+ ): LeafCstNode | undefined {
55
+ // Langium's "before" still returns a token that *starts* at the offset, so
56
+ // asking for the one that ends at the cursor means asking one back.
57
+ const where = prefer === "before" ? offset - 1 : offset;
58
+ return (
59
+ CstUtils.findLeafNodeAtOffset(root, where) ?? CstUtils.findLeafNodeBeforeOffset(root, where)
60
+ );
61
+ }
@@ -0,0 +1,91 @@
1
+ import { type Document, isFragmentDecl, isValueImport } from "@venn-lang/core";
2
+ import { type LangiumDocument, type LangiumDocuments, URI } from "langium";
3
+ import type { ImportResolver } from "../workspace/index.js";
4
+
5
+ export interface ModuleGraphScope {
6
+ root: Document;
7
+ uri: URI;
8
+ documents: LangiumDocuments;
9
+ imports: ImportResolver;
10
+ }
11
+
12
+ /** The files an import graph reaches, and how one names another. */
13
+ export interface ModuleGraph {
14
+ modules: ReadonlyMap<string, Document>;
15
+ resolve: (from: string, spec: string) => string;
16
+ }
17
+
18
+ /**
19
+ * Every module this file reaches, read straight out of the workspace index.
20
+ *
21
+ * Synchronous on purpose, for the same reason the decorators are: the checker
22
+ * runs on every keystroke and cannot await a file load. A neighbour the
23
+ * workspace has not indexed yet contributes nothing this pass and is picked up
24
+ * on the next, which is a moment of silence rather than a wrong answer.
25
+ *
26
+ * Walked transitively, because a `pub fn` that calls another file has the
27
+ * signature it really has only once that file has been read too.
28
+ */
29
+ export function importedModules(scope: ModuleGraphScope): ModuleGraph {
30
+ const modules = new Map<string, Document>();
31
+ const resolve = (from: string, spec: string): string =>
32
+ scope.imports.resolve(spec, URI.parse(from)).toString();
33
+ walk({ document: scope.root, uri: scope.uri.toString(), modules, resolve, scope });
34
+ return { modules, resolve };
35
+ }
36
+
37
+ interface WalkArgs {
38
+ document: Document;
39
+ uri: string;
40
+ modules: Map<string, Document>;
41
+ resolve: (from: string, spec: string) => string;
42
+ scope: ModuleGraphScope;
43
+ }
44
+
45
+ function walk(args: WalkArgs): void {
46
+ for (const decl of args.document.imports) {
47
+ if (!isValueImport(decl)) continue;
48
+ const target = args.resolve(args.uri, decl.path);
49
+ if (args.modules.has(target)) continue;
50
+ const root = rootAt(target, args.scope);
51
+ if (!root) continue;
52
+ args.modules.set(target, root);
53
+ walk({ ...args, document: root, uri: target });
54
+ }
55
+ }
56
+
57
+ function rootAt(uri: string, scope: ModuleGraphScope): Document | undefined {
58
+ const document = scope.documents.getDocument(URI.parse(uri));
59
+ return document && rootOf(document);
60
+ }
61
+
62
+ function rootOf(document: LangiumDocument): Document | undefined {
63
+ return document.parseResult?.value as Document | undefined;
64
+ }
65
+
66
+ /**
67
+ * Which of the names a file imports are fragments.
68
+ *
69
+ * The answer comes from what the neighbouring files declare, never from the
70
+ * import list alone: `run` accepts only a fragment, and a `pub fn` is not one.
71
+ */
72
+ export function importedFragments(args: {
73
+ document: Document;
74
+ uri: string;
75
+ graph: ModuleGraph;
76
+ }): Set<string> {
77
+ const found = new Set<string>();
78
+ for (const decl of args.document.imports) {
79
+ if (!isValueImport(decl)) continue;
80
+ const module = args.graph.modules.get(args.graph.resolve(args.uri, decl.path));
81
+ if (!module) continue;
82
+ for (const name of decl.names) {
83
+ if (declaresFragment(module, name)) found.add(name);
84
+ }
85
+ }
86
+ return found;
87
+ }
88
+
89
+ function declaresFragment(module: Document, name: string): boolean {
90
+ return module.decls.some((decl) => isFragmentDecl(decl) && decl.export && decl.name === name);
91
+ }
@@ -0,0 +1,15 @@
1
+ import { type Document, isValueImport, type ValueImport } from "@venn-lang/core";
2
+
3
+ /** The names a document pulls in via `import { a, b } from "…"`. */
4
+ export function importedNames(document: Document): string[] {
5
+ const names: string[] = [];
6
+ for (const decl of document.imports) {
7
+ if (isValueImport(decl)) names.push(...namesOf(decl));
8
+ }
9
+ return names;
10
+ }
11
+
12
+ function namesOf(decl: ValueImport): string[] {
13
+ if (decl.names.length > 0) return decl.names;
14
+ return decl.default ? [decl.default] : [];
15
+ }
@@ -0,0 +1,19 @@
1
+ export { type ExportedName, exportedNames } from "./exported-names.js";
2
+ export { pathOf, stdlibAction } from "./expression-path.js";
3
+ export { findBinding, findDeclaration, findFragment } from "./find-binding.js";
4
+ export type { HostArgs, SlotSource } from "./host-at.js";
5
+ export { hostAt } from "./host-at.js";
6
+ export {
7
+ importedFragments,
8
+ importedModules,
9
+ type ModuleGraph,
10
+ } from "./imported-modules.js";
11
+ export { importedNames } from "./imported-names.js";
12
+ export type { InterpolationHit, SlotHit } from "./interpolation-at.js";
13
+ export { interpolationAt, slotAt } from "./interpolation-at.js";
14
+ export type { ScopedName } from "./names-in-scope.js";
15
+ export { namesInScope } from "./names-in-scope.js";
16
+ export type { FragmentLocation, ResolveFragmentArgs } from "./resolve-fragment.js";
17
+ export { resolveFragment } from "./resolve-fragment.js";
18
+ export type { ImportedLocation, ResolveImportedArgs } from "./resolve-imported.js";
19
+ export { resolveImported } from "./resolve-imported.js";
@@ -0,0 +1,134 @@
1
+ import {
2
+ type AstNode,
3
+ type InterpolationSlot,
4
+ isStringLit,
5
+ scanInterpolations,
6
+ } from "@venn-lang/core";
7
+ import { CstUtils, type LangiumDocument } from "langium";
8
+
9
+ /** A dotted path of identifiers, e.g. `env.KEYCLOAK_URL`. */
10
+ const PATH = /[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*/g;
11
+
12
+ /** What the cursor is pointing at inside a `${…}` placeholder. */
13
+ export interface InterpolationHit {
14
+ /** The string literal holding the placeholder: the anchor for scope lookup. */
15
+ host: AstNode;
16
+ /** The whole dotted path under the cursor. */
17
+ path: string;
18
+ /** Just the segment under the cursor. */
19
+ name: string;
20
+ /** True on the first segment, the only one a scope can bind. */
21
+ isHead: boolean;
22
+ /** Which dotted segment the cursor is on: 0 is the head. */
23
+ segment: number;
24
+ /** Absolute offset and length of {@link name}, for ranges the editor needs. */
25
+ offset: number;
26
+ length: number;
27
+ /** Which `${…}` of the string this is: the index inference recorded it under. */
28
+ slot: number;
29
+ /** Where the cursor sits inside that placeholder's source. */
30
+ inner: number;
31
+ }
32
+
33
+ /**
34
+ * Resolve an offset that falls inside an interpolated string. Without this the
35
+ * editor treats `"${base}/users"` as prose: no hover, no jump, no rename.
36
+ */
37
+ export function interpolationAt(args: {
38
+ document: LangiumDocument;
39
+ offset: number;
40
+ }): InterpolationHit | undefined {
41
+ const found = slotAt(args);
42
+ if (!found) return undefined;
43
+ const { host, slot, index, inner } = found;
44
+ const hit = hitIn({ host, slot, at: inner, base: found.base });
45
+ return hit && { ...hit, slot: index, inner };
46
+ }
47
+
48
+ /** Which `${…}` the offset falls in, whether or not a name sits under it. */
49
+ export interface SlotHit {
50
+ host: AstNode;
51
+ slot: InterpolationSlot;
52
+ /** The placeholder's position among the string's, as inference recorded it. */
53
+ index: number;
54
+ /** Where the cursor sits inside the placeholder's source. */
55
+ inner: number;
56
+ /** Absolute offset of the placeholder's source, for ranges the editor needs. */
57
+ base: number;
58
+ }
59
+
60
+ /**
61
+ * Locate the placeholder alone. Completion asks this after a dot, where there
62
+ * is no name yet and {@link interpolationAt} would find nothing to describe.
63
+ */
64
+ export function slotAt(args: { document: LangiumDocument; offset: number }): SlotHit | undefined {
65
+ const host = stringAt(args);
66
+ const cst = host?.$cstNode;
67
+ if (!host || !cst) return undefined;
68
+ const local = args.offset - cst.offset;
69
+ const slots = scanInterpolations(cst.text);
70
+ const index = slots.findIndex((each) => inside(each, local));
71
+ const slot = slots[index];
72
+ if (!slot) return undefined;
73
+ return {
74
+ host,
75
+ slot,
76
+ index,
77
+ inner: local - slot.sourceStart,
78
+ base: cst.offset + slot.sourceStart,
79
+ };
80
+ }
81
+
82
+ /** A hit before the placeholder it came from is known. */
83
+ type Located = Omit<InterpolationHit, "slot" | "inner">;
84
+
85
+ function inside(slot: InterpolationSlot, offset: number): boolean {
86
+ return offset > slot.start && offset < slot.end;
87
+ }
88
+
89
+ function stringAt(args: { document: LangiumDocument; offset: number }): AstNode | undefined {
90
+ const root = args.document.parseResult?.value?.$cstNode;
91
+ const leaf = root && CstUtils.findLeafNodeAtOffset(root, args.offset);
92
+ const node = leaf?.astNode;
93
+ return node && isStringLit(node) ? node : undefined;
94
+ }
95
+
96
+ function hitIn(args: {
97
+ host: AstNode;
98
+ slot: InterpolationSlot;
99
+ at: number;
100
+ base: number;
101
+ }): Located | undefined {
102
+ for (const match of args.slot.source.matchAll(PATH)) {
103
+ const start = match.index;
104
+ if (args.at < start || args.at > start + match[0].length) continue;
105
+ return segmentAt({
106
+ host: args.host,
107
+ path: match[0],
108
+ base: args.base + start,
109
+ at: args.at - start,
110
+ });
111
+ }
112
+ return undefined;
113
+ }
114
+
115
+ /** Which dotted segment the cursor landed on, and where it sits in the document. */
116
+ function segmentAt(args: { host: AstNode; path: string; base: number; at: number }): Located {
117
+ const parts = args.path.split(".");
118
+ let cursor = 0;
119
+ let index = 0;
120
+ while (index < parts.length - 1 && args.at > cursor + (parts[index]?.length ?? 0)) {
121
+ cursor += (parts[index]?.length ?? 0) + 1;
122
+ index += 1;
123
+ }
124
+ const name = parts[index] ?? args.path;
125
+ return {
126
+ host: args.host,
127
+ path: args.path,
128
+ name,
129
+ isHead: index === 0,
130
+ segment: index,
131
+ offset: args.base + cursor,
132
+ length: name.length,
133
+ };
134
+ }
@@ -0,0 +1,126 @@
1
+ import {
2
+ type AstNode,
3
+ type Block,
4
+ type Document,
5
+ isBlock,
6
+ isCaptureStmt,
7
+ isDatasetDecl,
8
+ isDecoDecl,
9
+ isDocument,
10
+ isFnBody,
11
+ isFnDecl,
12
+ isFnExpr,
13
+ isForEachStmt,
14
+ isFragmentDecl,
15
+ isLetStmt,
16
+ isRepeatStmt,
17
+ isValueImport,
18
+ type ParamList,
19
+ } from "@venn-lang/core";
20
+
21
+ /** A name the program has bound, and the node that bound it. */
22
+ export interface ScopedName {
23
+ name: string;
24
+ node: AstNode;
25
+ /** How it came to be here: `const`, `parameter`, `fragment`, `resource`. */
26
+ origin: string;
27
+ }
28
+
29
+ /**
30
+ * Every name visible from `from`, innermost first.
31
+ *
32
+ * The mirror of `findBinding`, which answers about one name at a time. Asking
33
+ * the other way round is what lets the editor offer what is actually there:
34
+ * `http.on ` suggests the server two lines up, not every namespace loaded.
35
+ */
36
+ export function namesInScope(from: AstNode, at?: number): ScopedName[] {
37
+ const found: ScopedName[] = [];
38
+ const seen = new Set<string>();
39
+ let node: AstNode | undefined = from;
40
+ while (node) {
41
+ for (const each of bindingsIn(node)) {
42
+ if (seen.has(each.name) || beingWritten(each, at)) continue;
43
+ seen.add(each.name);
44
+ found.push(each);
45
+ }
46
+ node = node.$container;
47
+ }
48
+ return found;
49
+ }
50
+
51
+ /**
52
+ * Whether the cursor is inside what this binding is being given.
53
+ *
54
+ * `const t = saudacao(▮)` must not offer `t`: nothing holds it yet, and
55
+ * accepting it would write a definition in terms of itself.
56
+ */
57
+ function beingWritten(scoped: ScopedName, at: number | undefined): boolean {
58
+ if (at === undefined) return false;
59
+ const value = (isLetStmt(scoped.node) || isDatasetDecl(scoped.node)) && scoped.node.value;
60
+ const cst = value ? value.$cstNode : undefined;
61
+ return Boolean(cst && at >= cst.offset && at <= cst.end);
62
+ }
63
+
64
+ function bindingsIn(node: AstNode): ScopedName[] {
65
+ if (isForEachStmt(node)) return [{ name: node.item, node, origin: "each" }];
66
+ if (isRepeatStmt(node)) return node.index ? [{ name: node.index, node, origin: "index" }] : [];
67
+ if (isFragmentDecl(node) || isFnDecl(node) || isFnExpr(node) || isDecoDecl(node))
68
+ return params(node.params);
69
+ if (isFnBody(node)) return node.locals.map((local) => named(local, local.name, "let"));
70
+ if (isBlock(node)) return blockNames(node);
71
+ if (isDocument(node)) return documentNames(node);
72
+ return [];
73
+ }
74
+
75
+ function blockNames(block: Block): ScopedName[] {
76
+ return block.stmts
77
+ .filter((stmt) => isLetStmt(stmt) || isCaptureStmt(stmt))
78
+ .map((stmt) => named(stmt, (stmt as { name: string }).name, kindOf(stmt)));
79
+ }
80
+
81
+ function documentNames(document: Document): ScopedName[] {
82
+ const declared = document.decls.filter(declares).map((decl) => {
83
+ const name = (decl as unknown as { name: string }).name;
84
+ return named(decl, name, kindOf(decl));
85
+ });
86
+ return [...declared, ...importedInto(document)];
87
+ }
88
+
89
+ /**
90
+ * The names this file pulled in from another.
91
+ *
92
+ * An imported name is bound as firmly as a local one: it can be called, passed
93
+ * and held, so it belongs in scope like any other.
94
+ */
95
+ function importedInto(document: Document): ScopedName[] {
96
+ const found: ScopedName[] = [];
97
+ for (const decl of document.imports) {
98
+ if (!isValueImport(decl)) continue;
99
+ const names = decl.wildcard ? [decl.wildcard] : decl.names;
100
+ for (const name of names) found.push(named(decl, name, "import"));
101
+ }
102
+ return found;
103
+ }
104
+
105
+ /**
106
+ * A `fn` counts: it is a value the file bound, and passing one by name is how
107
+ * `http.on(api, route)` is written.
108
+ */
109
+ function declares(decl: AstNode): boolean {
110
+ return isLetStmt(decl) || isDatasetDecl(decl) || isFragmentDecl(decl) || isFnDecl(decl);
111
+ }
112
+
113
+ function kindOf(node: AstNode): string {
114
+ if (isLetStmt(node)) return node.kind;
115
+ if (isDatasetDecl(node)) return "dataset";
116
+ if (isFragmentDecl(node)) return "fragment";
117
+ return isFnDecl(node) ? "fn" : "let";
118
+ }
119
+
120
+ function params(list: ParamList | undefined): ScopedName[] {
121
+ return (list?.params ?? []).map((param) => named(param, param.name, "parameter"));
122
+ }
123
+
124
+ function named(node: AstNode, name: string, origin: string): ScopedName {
125
+ return { name, node, origin };
126
+ }
@@ -0,0 +1,38 @@
1
+ import { type Document, type FragmentDecl, isValueImport } from "@venn-lang/core";
2
+ import type { LangiumDocument, LangiumDocuments, URI } from "langium";
3
+ import type { ImportResolver } from "../workspace/index.js";
4
+ import { findFragment } from "./find-binding.js";
5
+
6
+ /** Where a fragment lives. `decl` is absent when the file could not be read. */
7
+ export interface FragmentLocation {
8
+ uri: URI;
9
+ decl?: FragmentDecl;
10
+ document?: LangiumDocument;
11
+ }
12
+
13
+ export interface ResolveFragmentArgs {
14
+ name: string;
15
+ document: LangiumDocument;
16
+ documents: LangiumDocuments;
17
+ imports: ImportResolver;
18
+ }
19
+
20
+ /** Find a fragment: declared in this file, or followed through the `import` naming it. */
21
+ export async function resolveFragment(
22
+ args: ResolveFragmentArgs,
23
+ ): Promise<FragmentLocation | undefined> {
24
+ const root = args.document.parseResult?.value as Document | undefined;
25
+ if (!root) return undefined;
26
+ const local = findFragment(root, args.name);
27
+ if (local) return { uri: args.document.uri, decl: local, document: args.document };
28
+ const decl = root.imports.find((node) => isValueImport(node) && node.names.includes(args.name));
29
+ return isValueImport(decl) ? fromImport(decl.path, args) : undefined;
30
+ }
31
+
32
+ async function fromImport(path: string, args: ResolveFragmentArgs): Promise<FragmentLocation> {
33
+ const uri = args.imports.resolve(path, args.document.uri);
34
+ const document = await args.documents.getOrCreateDocument(uri).catch(() => undefined);
35
+ const root = document?.parseResult?.value as Document | undefined;
36
+ const decl = root ? findFragment(root, args.name) : undefined;
37
+ return { uri, decl, document };
38
+ }
@@ -0,0 +1,35 @@
1
+ import type { AstNode, Document, ValueImport } from "@venn-lang/core";
2
+ import type { LangiumDocument, LangiumDocuments, URI } from "langium";
3
+ import type { ImportResolver } from "../workspace/index.js";
4
+ import { findDeclaration } from "./find-binding.js";
5
+
6
+ /** Where an imported name was declared. `decl` is absent when the file cannot be read. */
7
+ export interface ImportedLocation {
8
+ uri: URI;
9
+ decl?: AstNode;
10
+ document?: LangiumDocument;
11
+ }
12
+
13
+ export interface ResolveImportedArgs {
14
+ name: string;
15
+ /** The `import { … } from "…"` that names it. */
16
+ decl: ValueImport;
17
+ document: LangiumDocument;
18
+ documents: LangiumDocuments;
19
+ imports: ImportResolver;
20
+ }
21
+
22
+ /**
23
+ * Follow a name in an `import` list back to whatever declares it.
24
+ *
25
+ * Unlike `resolveFragment`, this does not care what kind of thing it finds: an
26
+ * import list holds `pub fn`, `pub fragment`, `pub deco` and `pub const` side
27
+ * by side with nothing distinguishing them, so the name is all there is to go
28
+ * on until the other file is read.
29
+ */
30
+ export async function resolveImported(args: ResolveImportedArgs): Promise<ImportedLocation> {
31
+ const uri = args.imports.resolve(args.decl.path, args.document.uri);
32
+ const document = await args.documents.getOrCreateDocument(uri).catch(() => undefined);
33
+ const root = document?.parseResult?.value as Document | undefined;
34
+ return { uri, decl: root ? findDeclaration(root, args.name) : undefined, document };
35
+ }
@@ -0,0 +1 @@
1
+ export { waitingFns } from "./waiting-fns.js";
@@ -0,0 +1,83 @@
1
+ import {
2
+ type AstNode,
3
+ type Document,
4
+ type FnDecl,
5
+ isCall,
6
+ isFnDecl,
7
+ isMember,
8
+ isRef,
9
+ } from "@venn-lang/core";
10
+ import { AstUtils } from "langium";
11
+ import type { SymbolCatalog } from "../catalog/index.js";
12
+
13
+ /**
14
+ * Which functions in a file wait for something.
15
+ *
16
+ * A plugin verb runs asynchronously, so anything that reaches for one hands
17
+ * back a value still arriving, and so does anything that calls that. The
18
+ * runtime does the waiting without a word being written; this is what lets the
19
+ * hover say so.
20
+ *
21
+ * It lives here rather than in the checker because deciding whether a dotted
22
+ * path names a verb needs the plugin registry, and `@venn-lang/core` deliberately
23
+ * knows nothing about plugins. Being an editor's answer, it may be approximate:
24
+ * missing one costs a hint, never a wrong program.
25
+ */
26
+ export function waitingFns(document: Document, catalog: SymbolCatalog): ReadonlySet<string> {
27
+ const bodies = declaredFns(document);
28
+ const waiting = new Set<string>();
29
+ for (const [name, decl] of bodies) {
30
+ if (callsAVerb(decl, catalog)) waiting.add(name);
31
+ }
32
+ spread(bodies, waiting);
33
+ return waiting;
34
+ }
35
+
36
+ function declaredFns(document: Document): Map<string, FnDecl> {
37
+ const out = new Map<string, FnDecl>();
38
+ for (const decl of document.decls) {
39
+ if (isFnDecl(decl)) out.set(decl.name, decl);
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /** A verb somewhere in the body, whether named or called. */
45
+ function callsAVerb(decl: FnDecl, catalog: SymbolCatalog): boolean {
46
+ for (const node of AstUtils.streamAst(decl.body)) {
47
+ const head = pathHead(node);
48
+ if (head && catalog.hasNamespace(head)) return true;
49
+ }
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Then outwards, until nothing new is found: a function that calls one that
55
+ * waits, waits. Bounded by the number of functions, so it always settles.
56
+ */
57
+ function spread(bodies: Map<string, FnDecl>, waiting: Set<string>): void {
58
+ for (let pass = 0; pass < bodies.size; pass += 1) {
59
+ let grew = false;
60
+ for (const [name, decl] of bodies) {
61
+ if (waiting.has(name)) continue;
62
+ if (!callsAnyOf(decl, waiting)) continue;
63
+ waiting.add(name);
64
+ grew = true;
65
+ }
66
+ if (!grew) return;
67
+ }
68
+ }
69
+
70
+ function callsAnyOf(decl: FnDecl, names: ReadonlySet<string>): boolean {
71
+ for (const node of AstUtils.streamAst(decl.body)) {
72
+ if (isCall(node) && isRef(node.callee) && names.has(node.callee.name)) return true;
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /** The first name of a dotted path: `http` in `http.get`, `fmt` in `fmt.json`. */
78
+ function pathHead(node: AstNode): string | undefined {
79
+ if (!isMember(node)) return undefined;
80
+ let receiver = node.receiver;
81
+ while (isMember(receiver)) receiver = receiver.receiver;
82
+ return isRef(receiver) ? receiver.name : undefined;
83
+ }