@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
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@venn-lang/lsp",
3
+ "version": "0.1.0",
4
+ "description": "The Venn language server: diagnostics, hover, completion, definition, references, rename, signature help, semantic tokens, formatting, document highlight and quick fixes for .vn files.",
5
+ "keywords": [
6
+ "venn",
7
+ "testing",
8
+ "e2e",
9
+ "lsp",
10
+ "language-server",
11
+ "editor"
12
+ ],
13
+ "homepage": "https://github.com/venn-lang/venn/tree/main/packages/lsp#readme",
14
+ "bugs": "https://github.com/venn-lang/venn/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/venn-lang/venn.git",
18
+ "directory": "packages/lsp"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Vinicius Borges",
22
+ "type": "module",
23
+ "bin": {
24
+ "venn-lsp": "./dist/bin/venn-lsp.mjs"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "development": "./src/index.ts",
29
+ "types": "./dist/index.d.mts",
30
+ "import": "./dist/index.mjs",
31
+ "default": "./dist/index.mjs"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src",
37
+ "!src/**/*.test.ts",
38
+ "!src/**/*.suite.ts"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "langium": "^4.3.1",
45
+ "vscode-languageserver": "^10.0.1",
46
+ "vscode-languageserver-textdocument": "^1.0.13",
47
+ "@venn-lang/contracts": "0.1.0",
48
+ "@venn-lang/core": "0.1.0",
49
+ "@venn-lang/runtime": "0.1.0",
50
+ "@venn-lang/sdk": "0.1.0",
51
+ "@venn-lang/stdlib": "0.1.0",
52
+ "@venn-lang/types": "0.1.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^24",
56
+ "tsdown": "^0.22.14",
57
+ "typescript": "^7.0.2",
58
+ "vitest": "^4.1.10"
59
+ },
60
+ "scripts": {
61
+ "build": "tsdown",
62
+ "test": "vitest run",
63
+ "typecheck": "tsc --noEmit"
64
+ }
65
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { type Connection, createConnection, ProposedFeatures } from "vscode-languageserver/node";
3
+ import { startVennServer } from "../server/index.js";
4
+
5
+ const TRANSPORT_FLAGS = ["--stdio", "--node-ipc", "--socket"];
6
+
7
+ // Editors pass a transport flag; with none given, default to stdio so the server
8
+ // is usable straight from a shell.
9
+ function connect(): Connection {
10
+ const flagged = process.argv.some((arg) => TRANSPORT_FLAGS.some((flag) => arg.startsWith(flag)));
11
+ const connection = flagged
12
+ ? createConnection(ProposedFeatures.all)
13
+ : createConnection(ProposedFeatures.all, process.stdin, process.stdout);
14
+ return connection as Connection;
15
+ }
16
+
17
+ startVennServer(connect());
@@ -0,0 +1,72 @@
1
+ import type { PluginDefinition } from "@venn-lang/sdk";
2
+ import type { ActionEntry, MatcherEntry, SymbolCatalog, TypeEntry } from "./catalog.types.js";
3
+
4
+ interface Index {
5
+ actions: Map<string, ActionEntry>;
6
+ matchers: Map<string, MatcherEntry>;
7
+ byNamespace: Map<string, ActionEntry[]>;
8
+ typesByNamespace: Map<string, TypeEntry[]>;
9
+ packages: Map<string, string>;
10
+ }
11
+
12
+ /**
13
+ * Index the loaded plugins so completion, hover and tokens can look symbols up.
14
+ *
15
+ * @param plugins The plugin definitions loaded for the workspace.
16
+ * @returns A read-only view over their actions, matchers and published types.
17
+ */
18
+ export function buildCatalog(plugins: readonly PluginDefinition[]): SymbolCatalog {
19
+ const index: Index = {
20
+ actions: new Map(),
21
+ matchers: new Map(),
22
+ byNamespace: new Map(),
23
+ typesByNamespace: new Map(),
24
+ packages: new Map(),
25
+ };
26
+ for (const plugin of plugins) addPlugin(plugin, index);
27
+ return view(index, plugins);
28
+ }
29
+
30
+ function view(index: Index, plugins: readonly PluginDefinition[]): SymbolCatalog {
31
+ return {
32
+ namespaces: () => [...index.byNamespace.keys()],
33
+ packages: () => plugins.map((plugin) => plugin.name),
34
+ hasNamespace: (namespace) => index.byNamespace.has(namespace),
35
+ actionsIn: (namespace) => index.byNamespace.get(namespace) ?? [],
36
+ action: (namespace, name) => index.actions.get(`${namespace}.${name}`),
37
+ typesIn: (namespace) => index.typesByNamespace.get(namespace) ?? [],
38
+ matchers: () => [...index.matchers.values()],
39
+ matcher: (name) => index.matchers.get(name),
40
+ namespaceOfPackage: (pkg) => index.packages.get(pkg),
41
+ packagesFor: (namespace) =>
42
+ [...index.packages.entries()]
43
+ .filter(([, contributed]) => contributed === namespace)
44
+ .map(([pkg]) => pkg),
45
+ };
46
+ }
47
+
48
+ function addPlugin(plugin: PluginDefinition, index: Index): void {
49
+ index.packages.set(plugin.name, plugin.namespace);
50
+ const list = index.byNamespace.get(plugin.namespace) ?? [];
51
+ index.byNamespace.set(plugin.namespace, list);
52
+ for (const action of plugin.actions ?? []) {
53
+ const entry = { namespace: plugin.namespace, name: action.name, package: plugin.name, action };
54
+ index.actions.set(`${plugin.namespace}.${action.name}`, entry);
55
+ list.push(entry);
56
+ }
57
+ for (const matcher of plugin.matchers ?? []) {
58
+ index.matchers.set(matcher.name, { name: matcher.name, package: plugin.name, matcher });
59
+ }
60
+ addTypes(plugin, index);
61
+ }
62
+
63
+ /** A namespace offers its published types alongside its verbs: `http.Request`. */
64
+ function addTypes(plugin: PluginDefinition, index: Index): void {
65
+ const entries = Object.entries(plugin.typeDefs ?? {});
66
+ if (entries.length === 0) return;
67
+ const list = index.typesByNamespace.get(plugin.namespace) ?? [];
68
+ for (const [name, spec] of entries) {
69
+ list.push({ namespace: plugin.namespace, name, package: plugin.name, spec });
70
+ }
71
+ index.typesByNamespace.set(plugin.namespace, list);
72
+ }
@@ -0,0 +1,44 @@
1
+ import type { ActionDefinition, MatcherDefinition } from "@venn-lang/sdk";
2
+ import type { TypeSpec } from "@venn-lang/types";
3
+
4
+ /** One type a plugin publishes: `http.Request`, and the shape behind it. */
5
+ export interface TypeEntry {
6
+ namespace: string;
7
+ name: string;
8
+ package: string;
9
+ spec: TypeSpec;
10
+ }
11
+
12
+ /** One action, with the package that contributes it. */
13
+ export interface ActionEntry {
14
+ namespace: string;
15
+ name: string;
16
+ package: string;
17
+ action: ActionDefinition;
18
+ }
19
+
20
+ /** One matcher, with the package that contributes it. */
21
+ export interface MatcherEntry {
22
+ name: string;
23
+ package: string;
24
+ matcher: MatcherDefinition;
25
+ }
26
+
27
+ /**
28
+ * Everything the editor needs to resolve, list and describe stdlib symbols.
29
+ * The grammar cannot tell `http.post` from `myHelper.foo`; this catalogue can.
30
+ */
31
+ export interface SymbolCatalog {
32
+ namespaces(): readonly string[];
33
+ packages(): readonly string[];
34
+ hasNamespace(namespace: string): boolean;
35
+ actionsIn(namespace: string): readonly ActionEntry[];
36
+ action(namespace: string, name: string): ActionEntry | undefined;
37
+ /** The types that namespace publishes: what `http.` offers besides verbs. */
38
+ typesIn(namespace: string): readonly TypeEntry[];
39
+ matchers(): readonly MatcherEntry[];
40
+ matcher(name: string): MatcherEntry | undefined;
41
+ namespaceOfPackage(pkg: string): string | undefined;
42
+ /** Every package contributing this namespace: the `use` options to offer. */
43
+ packagesFor(namespace: string): readonly string[];
44
+ }
@@ -0,0 +1,2 @@
1
+ export { buildCatalog } from "./build-catalog.js";
2
+ export type { ActionEntry, MatcherEntry, SymbolCatalog } from "./catalog.types.js";
@@ -0,0 +1,49 @@
1
+ import { type Document, type ImportDecl, isUseDecl, isValueImport } from "@venn-lang/core";
2
+ import type { LangiumDocument } from "langium";
3
+ import type { Position, TextEdit } from "vscode-languageserver";
4
+
5
+ /** Which header group a new line joins. `use` always precedes `import`. */
6
+ export type HeaderKind = "use" | "import";
7
+
8
+ /**
9
+ * Insert a header line after the last one of its own kind, so `use` lines stay
10
+ * grouped above `import` lines rather than landing wherever the header ends.
11
+ */
12
+ export function headerEdit(document: LangiumDocument, line: string, kind: HeaderKind): TextEdit {
13
+ const position = anchor(document, kind);
14
+ return { range: { start: position, end: position }, newText: `${line}\n` };
15
+ }
16
+
17
+ function anchor(document: LangiumDocument, kind: HeaderKind): Position {
18
+ const root = document.parseResult?.value as Document | undefined;
19
+ const imports = root?.imports ?? [];
20
+ const preferred = last(imports, kind === "use" ? isUseDecl : isValueImport);
21
+ const fallback = kind === "import" ? last(imports, isUseDecl) : undefined;
22
+ const end = endOf(preferred ?? fallback) ?? moduleEnd(root);
23
+ if (end === undefined) return { line: 0, character: 0 };
24
+ return { line: document.textDocument.positionAt(end).line + 1, character: 0 };
25
+ }
26
+
27
+ function last(
28
+ imports: readonly ImportDecl[],
29
+ is: (node: ImportDecl) => boolean,
30
+ ): ImportDecl | undefined {
31
+ return [...imports].reverse().find((decl) => is(decl));
32
+ }
33
+
34
+ function endOf(decl: ImportDecl | undefined): number | undefined {
35
+ const cst = decl?.$cstNode;
36
+ return cst ? cst.offset + cst.length : undefined;
37
+ }
38
+
39
+ function moduleEnd(root: Document | undefined): number | undefined {
40
+ if (!root?.name || !root.$cstNode) return undefined;
41
+ const newline = root.$cstNode.text.indexOf("\n");
42
+ return newline < 0 ? undefined : root.$cstNode.offset + newline;
43
+ }
44
+
45
+ /** True when the file already has an `import` naming this module path. */
46
+ export function alreadyImports(document: LangiumDocument, path: string): boolean {
47
+ const root = document.parseResult?.value as Document | undefined;
48
+ return (root?.imports ?? []).some((decl) => isValueImport(decl) && decl.path === path);
49
+ }
@@ -0,0 +1,139 @@
1
+ import {
2
+ type AstNode,
3
+ isActionCall,
4
+ isLetStmt,
5
+ isMatcherClause,
6
+ isRunStmt,
7
+ isStringLit,
8
+ } from "@venn-lang/core";
9
+ import { CstUtils, type LangiumDocument } from "langium";
10
+ import type { CodeActionProvider } from "langium/lsp";
11
+ import {
12
+ type CodeAction,
13
+ CodeActionKind,
14
+ type CodeActionParams,
15
+ type Diagnostic,
16
+ type TextEdit,
17
+ } from "vscode-languageserver";
18
+ import type { SymbolCatalog } from "../catalog/index.js";
19
+ import { pathOf } from "../document/index.js";
20
+ import type { VennServices } from "../services/lsp.types.js";
21
+ import type { ImportResolver } from "../workspace/index.js";
22
+ import { alreadyImports, headerEdit } from "./anchor.js";
23
+ import { modulesExporting } from "./exporting-modules.js";
24
+
25
+ interface FixArgs {
26
+ title: string;
27
+ edit: TextEdit;
28
+ document: LangiumDocument;
29
+ diagnostic: Diagnostic;
30
+ preferred: boolean;
31
+ }
32
+
33
+ /**
34
+ * Lightbulb fixes for the two "you never brought this in" diagnostics. When more
35
+ * than one source provides the name, every source is offered and the first is
36
+ * marked preferred, so the editor shows it first and applies it on Quick Fix.
37
+ */
38
+ export class VennCodeActionProvider implements CodeActionProvider {
39
+ private readonly catalog: SymbolCatalog;
40
+ private readonly imports: ImportResolver;
41
+
42
+ constructor(services: VennServices) {
43
+ this.catalog = services.catalog;
44
+ this.imports = services.imports;
45
+ }
46
+
47
+ getCodeActions(document: LangiumDocument, params: CodeActionParams): CodeAction[] {
48
+ return params.context.diagnostics.flatMap((diagnostic) => this.fixes(diagnostic, document));
49
+ }
50
+
51
+ private fixes(diagnostic: Diagnostic, document: LangiumDocument): CodeAction[] {
52
+ if (diagnostic.code === "VN2007") return this.addUse(diagnostic, document);
53
+ if (diagnostic.code === "VN2005") return this.addImport(diagnostic, document);
54
+ if (diagnostic.code === "VN5001") return this.useLet(diagnostic, document);
55
+ return [];
56
+ }
57
+
58
+ private addUse(diagnostic: Diagnostic, document: LangiumDocument): CodeAction[] {
59
+ const node = nodeAt(document, diagnostic);
60
+ return this.sources(node).map((pkg, index) =>
61
+ fix({
62
+ title: `Add use "${pkg}"`,
63
+ edit: headerEdit(document, `use "${pkg}"`, "use"),
64
+ document,
65
+ diagnostic,
66
+ preferred: index === 0,
67
+ }),
68
+ );
69
+ }
70
+
71
+ /** Every package that would bring this name in, as action, matcher or call. */
72
+ private sources(node: AstNode | undefined): readonly string[] {
73
+ if (!node) return [];
74
+ if (isMatcherClause(node)) {
75
+ const found = this.catalog.matcher(node.name);
76
+ return found ? [found.package] : [];
77
+ }
78
+ // The read may hide inside `${…}`, where the node is the string, not a path.
79
+ if (isStringLit(node)) return this.interpolatedSources(node);
80
+ // A bare path counts too: `env.BASE_URL` is a read, and it still needs a `use`.
81
+ const path = isActionCall(node) ? node.target : pathOf(isLetStmt(node) ? node.value : node);
82
+ const namespace = path?.split(".")[0];
83
+ return namespace ? this.catalog.packagesFor(namespace) : [];
84
+ }
85
+
86
+ /** The package for the first namespace read in `"…${env.X}…"` we recognise. */
87
+ private interpolatedSources(node: { value?: string }): readonly string[] {
88
+ for (const match of (node.value ?? "").matchAll(/\$\{\s*([A-Za-z_]\w*)\./g)) {
89
+ const packages = this.catalog.packagesFor(match[1] ?? "");
90
+ if (packages.length > 0) return packages;
91
+ }
92
+ return [];
93
+ }
94
+
95
+ /** `capture` is spelled `let`, so the fix is exactly one word wide. */
96
+ private useLet(diagnostic: Diagnostic, document: LangiumDocument): CodeAction[] {
97
+ const { start } = diagnostic.range;
98
+ const end = { line: start.line, character: start.character + "capture".length };
99
+ const edit = { range: { start, end }, newText: "let" };
100
+ return [
101
+ fix({ title: "Replace `capture` with `let`", edit, document, diagnostic, preferred: true }),
102
+ ];
103
+ }
104
+
105
+ private addImport(diagnostic: Diagnostic, document: LangiumDocument): CodeAction[] {
106
+ const node = nodeAt(document, diagnostic);
107
+ if (!node || !isRunStmt(node)) return [];
108
+ const name = node.target;
109
+ const modules = modulesExporting({ name, base: document.uri, imports: this.imports });
110
+ return modules
111
+ .filter((module) => !alreadyImports(document, module.specifier))
112
+ .map((module, index) =>
113
+ fix({
114
+ title: `Import ${name} from "${module.specifier}"`,
115
+ edit: headerEdit(document, `import { ${name} } from "${module.specifier}"`, "import"),
116
+ document,
117
+ diagnostic,
118
+ preferred: index === 0,
119
+ }),
120
+ );
121
+ }
122
+ }
123
+
124
+ function fix(args: FixArgs): CodeAction {
125
+ return {
126
+ title: args.title,
127
+ kind: CodeActionKind.QuickFix,
128
+ diagnostics: [args.diagnostic],
129
+ isPreferred: args.preferred,
130
+ edit: { changes: { [args.document.uri.toString()]: [args.edit] } },
131
+ };
132
+ }
133
+
134
+ function nodeAt(document: LangiumDocument, diagnostic: Diagnostic): AstNode | undefined {
135
+ const root = document.parseResult?.value?.$cstNode;
136
+ if (!root) return undefined;
137
+ const offset = document.textDocument.offsetAt(diagnostic.range.start);
138
+ return CstUtils.findLeafNodeAtOffset(root, offset)?.astNode;
139
+ }
@@ -0,0 +1,73 @@
1
+ import { type Dirent, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parse } from "@venn-lang/core";
4
+ import { type URI, UriUtils } from "langium";
5
+ import { exportedNames } from "../document/index.js";
6
+ import type { ImportResolver } from "../workspace/index.js";
7
+
8
+ const EXTENSION = ".vn";
9
+ const MAX_DEPTH = 4;
10
+ const SKIP = new Set(["node_modules", "dist", ".git"]);
11
+
12
+ /** A module that exports the wanted name, and how to write its specifier. */
13
+ export interface ExportingModule {
14
+ specifier: string;
15
+ file: string;
16
+ }
17
+
18
+ /**
19
+ * Every `.vn` file near this one that marks `name` as `pub`. Specifiers prefer
20
+ * a configured `#alias`, falling back to a relative path.
21
+ */
22
+ export function modulesExporting(args: {
23
+ name: string;
24
+ base: URI;
25
+ imports: ImportResolver;
26
+ }): ExportingModule[] {
27
+ const aliases = args.imports.aliases(args.base);
28
+ const root = UriUtils.dirname(args.base);
29
+ const found: ExportingModule[] = [];
30
+ for (const file of walk(root.fsPath, "", 0)) {
31
+ if (!exportsName(join(root.fsPath, file), args.name)) continue;
32
+ found.push({ specifier: specifierFor(file, root, aliases), file });
33
+ }
34
+ return found;
35
+ }
36
+
37
+ function exportsName(path: string, name: string): boolean {
38
+ try {
39
+ const { ast, problems } = parse(readFileSync(path, "utf8"), { uri: path });
40
+ return problems.length === 0 && exportedNames(ast).some((each) => each.name === name);
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ // `#shared/auth.vn` reads better than `./shared/auth.vn` when an alias covers it.
47
+ function specifierFor(file: string, root: URI, aliases: Record<string, URI>): string {
48
+ const absolute = UriUtils.resolvePath(root, file).toString();
49
+ for (const [alias, folder] of Object.entries(aliases)) {
50
+ const prefix = `${folder.toString()}/`;
51
+ if (absolute.startsWith(prefix)) return `${alias}/${absolute.slice(prefix.length)}`;
52
+ }
53
+ return `./${file}`;
54
+ }
55
+
56
+ function walk(root: string, relative: string, depth: number): string[] {
57
+ if (depth > MAX_DEPTH) return [];
58
+ const found: string[] = [];
59
+ for (const entry of read(join(root, relative))) {
60
+ const child = relative ? `${relative}/${entry.name}` : entry.name;
61
+ if (entry.isDirectory() && !SKIP.has(entry.name)) found.push(...walk(root, child, depth + 1));
62
+ else if (entry.isFile() && entry.name.endsWith(EXTENSION)) found.push(child);
63
+ }
64
+ return found;
65
+ }
66
+
67
+ function read(directory: string): Dirent[] {
68
+ try {
69
+ return readdirSync(directory, { withFileTypes: true });
70
+ } catch {
71
+ return [];
72
+ }
73
+ }
@@ -0,0 +1,5 @@
1
+ export type { HeaderKind } from "./anchor.js";
2
+ export { alreadyImports, headerEdit } from "./anchor.js";
3
+ export { VennCodeActionProvider } from "./code-actions.js";
4
+ export type { ExportingModule } from "./exporting-modules.js";
5
+ export { modulesExporting } from "./exporting-modules.js";
@@ -0,0 +1,81 @@
1
+ import { type AstNode, showType, type Type } from "@venn-lang/core";
2
+ import type { CompletionItem, Range } from "vscode-languageserver";
3
+ import type { SymbolCatalog } from "../catalog/index.js";
4
+ import type { ScopedName } from "../document/index.js";
5
+ import type { ShownArg } from "../signature/index.js";
6
+ import { ICON, iconForOrigin } from "./icons.js";
7
+ import { item } from "./items.js";
8
+
9
+ export interface ArgumentItemsArgs {
10
+ names: readonly ScopedName[];
11
+ types: ReadonlyMap<AstNode, Type>;
12
+ /** The argument being written, when the verb said what it takes. */
13
+ expects: ShownArg | undefined;
14
+ catalog: SymbolCatalog;
15
+ range: Range;
16
+ }
17
+
18
+ /**
19
+ * What can go here, most likely first.
20
+ *
21
+ * An argument almost always wants something the program already has: the server
22
+ * two lines up, the parameter of the enclosing function. Offering the loaded
23
+ * namespaces first, as a fresh statement would, buries the one answer under
24
+ * dozens that cannot be right.
25
+ */
26
+ export function argumentItems(args: ArgumentItemsArgs): CompletionItem[] {
27
+ const values = args.names.map((each) => valueItem(each, args));
28
+ const namespaces = args.catalog.namespaces().map((name) => nsItem(name, args.range));
29
+ return [...values, ...namespaces];
30
+ }
31
+
32
+ /**
33
+ * The names a program holds, offered where no type is expected of them.
34
+ *
35
+ * The start of a statement is such a place: nothing says what should go there,
36
+ * so nothing can be ranked as fitting, only drawn for what it is.
37
+ */
38
+ export function boundItems(names: readonly ScopedName[], range: Range): CompletionItem[] {
39
+ return names.map((scoped) => ({
40
+ ...item({
41
+ label: scoped.name,
42
+ kind: iconForOrigin(scoped.origin),
43
+ range,
44
+ detail: scoped.origin,
45
+ }),
46
+ sortText: `0${scoped.name}`,
47
+ }));
48
+ }
49
+
50
+ function valueItem(scoped: ScopedName, args: ArgumentItemsArgs): CompletionItem {
51
+ const type = args.types.get(scoped.node);
52
+ const shown = type ? showType(type) : undefined;
53
+ return {
54
+ ...item({
55
+ label: scoped.name,
56
+ kind: iconForOrigin(scoped.origin),
57
+ range: args.range,
58
+ detail: shown ? `${scoped.origin} ${scoped.name}: ${shown}` : scoped.origin,
59
+ }),
60
+ sortText: `${fits(shown, args.expects) ? "0" : "1"}${scoped.name}`,
61
+ };
62
+ }
63
+
64
+ function nsItem(name: string, range: Range): CompletionItem {
65
+ return {
66
+ ...item({ label: name, kind: ICON.namespace, range }),
67
+ sortText: `2${name}`,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Whether a value is the kind of thing this argument wants. Compared as the two
73
+ * read, not by unification: the checker's types carry inference variables that
74
+ * matching would have to bind, and ranking a list must not change what a
75
+ * program means.
76
+ */
77
+ function fits(shown: string | undefined, expects: ShownArg | undefined): boolean {
78
+ if (!shown || !expects) return false;
79
+ if (shown === expects.type) return true;
80
+ return expects.type.startsWith("fn(") && shown.startsWith("fn(");
81
+ }