@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,80 @@
1
+ import { isCall, isMember, isRef, MEMBER_DOCS } from "@venn-lang/core";
2
+ import { SemanticTokenModifiers, SemanticTokenTypes } from "vscode-languageserver";
3
+ import type { SymbolCatalog } from "../catalog/index.js";
4
+ import { findBinding, pathOf } from "../document/index.js";
5
+ import type { HighlightArgs } from "./highlight.types.js";
6
+
7
+ const LIB = SemanticTokenModifiers.defaultLibrary;
8
+
9
+ /** Every built-in member name, whatever kind of value it hangs off. */
10
+ const BUILT_IN = new Set(Object.values(MEMBER_DOCS).flatMap((table) => Object.keys(table)));
11
+
12
+ /**
13
+ * Colour what a `.` reaches. The parser cannot tell `http.post` from
14
+ * `res.status`, since both are member chains, so the catalog and the built-in
15
+ * tables decide: a plugin verb reads as a function, a built-in as a method, and
16
+ * anything else as the field it is.
17
+ *
18
+ * Returns whether it claimed the node; tokens must not overlap.
19
+ */
20
+ export function highlightPath(args: HighlightArgs): boolean {
21
+ const { node, acceptor } = args;
22
+ if (isRef(node)) return namespaceHead(node.name, args);
23
+ if (!isMember(node)) return false;
24
+ acceptor({ node, property: "member", ...memberToken(node.member, args) });
25
+ return true;
26
+ }
27
+
28
+ interface Token {
29
+ type: string;
30
+ modifier?: string | string[];
31
+ }
32
+
33
+ /** A plugin verb, a built-in method, or an ordinary field. */
34
+ function memberToken(member: string, args: HighlightArgs): Token {
35
+ const path = pathOf(args.node);
36
+ if (path && isStdlibPath(path, args)) return stdlibToken(path, args.catalog);
37
+ if (BUILT_IN.has(member)) {
38
+ return { type: methodOrProperty(args), modifier: LIB };
39
+ }
40
+ return { type: SemanticTokenTypes.property };
41
+ }
42
+
43
+ /** `xs.map(…)` is a method; `xs.len` is a property, read without parentheses. */
44
+ function methodOrProperty(args: HighlightArgs): string {
45
+ return isCall(args.node.$container) && args.node.$container.callee === args.node
46
+ ? SemanticTokenTypes.method
47
+ : SemanticTokenTypes.property;
48
+ }
49
+
50
+ /** The head of `http.post` is a namespace, not a variable nobody declared. */
51
+ function namespaceHead(name: string, args: HighlightArgs): boolean {
52
+ if (!isNamespace(name, args)) return false;
53
+ args.acceptor({
54
+ node: args.node,
55
+ property: "name",
56
+ type: SemanticTokenTypes.namespace,
57
+ modifier: LIB,
58
+ });
59
+ return true;
60
+ }
61
+
62
+ /** Scope wins: a variable named `auth` stays a variable even though `auth` is a namespace. */
63
+ function isNamespace(name: string, args: HighlightArgs): boolean {
64
+ return args.catalog.hasNamespace(name) && !findBinding(args.node, name);
65
+ }
66
+
67
+ /** The last segment is the verb when the whole path names an action. */
68
+ function stdlibToken(path: string, catalog: SymbolCatalog): Token {
69
+ const namespace = path.slice(0, path.indexOf("."));
70
+ const name = path.slice(path.indexOf(".") + 1);
71
+ const type = catalog.action(namespace, name)
72
+ ? SemanticTokenTypes.function
73
+ : SemanticTokenTypes.property;
74
+ return { type, modifier: LIB };
75
+ }
76
+
77
+ function isStdlibPath(path: string, args: HighlightArgs): boolean {
78
+ const dot = path.indexOf(".");
79
+ return dot > 0 && isNamespace(path.slice(0, dot), args);
80
+ }
@@ -0,0 +1,10 @@
1
+ import type { AstNode } from "@venn-lang/core";
2
+ import type { SemanticTokenAcceptor } from "langium/lsp";
3
+ import type { SymbolCatalog } from "../catalog/index.js";
4
+
5
+ /** What a highlight pass needs: the node, where to emit, and symbol resolution. */
6
+ export interface HighlightArgs {
7
+ node: AstNode;
8
+ acceptor: SemanticTokenAcceptor;
9
+ catalog: SymbolCatalog;
10
+ }
@@ -0,0 +1 @@
1
+ export { VennSemanticTokenProvider } from "./semantic-tokens.js";
@@ -0,0 +1,32 @@
1
+ import type { AstNode } from "langium";
2
+ import { AbstractSemanticTokenProvider, type SemanticTokenAcceptor } from "langium/lsp";
3
+ import type { SymbolCatalog } from "../catalog/index.js";
4
+ import type { VennServices } from "../services/lsp.types.js";
5
+ import { highlightCalls } from "./highlight-calls.js";
6
+ import { highlightKeywords } from "./highlight-keywords.js";
7
+ import { highlightLiterals } from "./highlight-literals.js";
8
+ import { highlightModule } from "./highlight-module.js";
9
+ import { highlightNames } from "./highlight-names.js";
10
+
11
+ /**
12
+ * Colours a `.vn` the way §23 specifies. The grammar cannot tell `http.post`
13
+ * from `myHelper.foo`, since both are `ActionCall`, so the catalog decides.
14
+ * Each pass owns one family of tokens, and a node matches at most one of them.
15
+ */
16
+ export class VennSemanticTokenProvider extends AbstractSemanticTokenProvider {
17
+ private readonly catalog: SymbolCatalog;
18
+
19
+ constructor(services: VennServices) {
20
+ super(services);
21
+ this.catalog = services.catalog;
22
+ }
23
+
24
+ protected override highlightElement(node: AstNode, acceptor: SemanticTokenAcceptor): void {
25
+ const args = { node, acceptor, catalog: this.catalog };
26
+ highlightKeywords(node, acceptor);
27
+ highlightModule(args);
28
+ highlightCalls(args);
29
+ highlightLiterals(args);
30
+ highlightNames(args);
31
+ }
32
+ }
@@ -0,0 +1 @@
1
+ export { startVennServer } from "./start-server.js";
@@ -0,0 +1,15 @@
1
+ import { startLanguageServer } from "langium/lsp";
2
+ import { NodeFileSystem } from "langium/node";
3
+ import type { Connection } from "vscode-languageserver";
4
+ import { createVennLspServices } from "../services/index.js";
5
+
6
+ /**
7
+ * Start the Venn language server on an established connection. This is the one
8
+ * wiring shared by the standalone `venn-lsp` binary and the VS Code extension.
9
+ *
10
+ * @param connection An LSP connection the caller has already created.
11
+ */
12
+ export function startVennServer(connection: Connection): void {
13
+ const { shared } = createVennLspServices({ connection, ...NodeFileSystem });
14
+ startLanguageServer(shared);
15
+ }
@@ -0,0 +1,2 @@
1
+ export type { VennAddedServices, VennServices } from "./lsp.types.js";
2
+ export { createVennLspServices } from "./venn-lsp-module.js";
@@ -0,0 +1,14 @@
1
+ import type { LangiumServices } from "langium/lsp";
2
+ import type { SymbolCatalog } from "../catalog/index.js";
3
+ import type { TypeService } from "../types/index.js";
4
+ import type { ImportResolver } from "../workspace/index.js";
5
+
6
+ /** The extra services Venn adds on top of Langium's LSP stack. */
7
+ export interface VennAddedServices {
8
+ catalog: SymbolCatalog;
9
+ imports: ImportResolver;
10
+ types: TypeService;
11
+ }
12
+
13
+ /** The full language services for Venn. */
14
+ export type VennServices = LangiumServices & VennAddedServices;
@@ -0,0 +1,65 @@
1
+ import { VennGeneratedModule, VennGeneratedSharedModule, VennLexer } from "@venn-lang/core";
2
+ import { allPlugins } from "@venn-lang/stdlib";
3
+ import { inject, type Module } from "langium";
4
+ import {
5
+ createDefaultModule,
6
+ createDefaultSharedModule,
7
+ type DefaultSharedModuleContext,
8
+ type LangiumSharedServices,
9
+ type PartialLangiumServices,
10
+ } from "langium/lsp";
11
+ import { buildCatalog } from "../catalog/index.js";
12
+ import { VennCodeActionProvider } from "../code-actions/index.js";
13
+ import { VennCompletionProvider } from "../completion/index.js";
14
+ import { VennDefinitionProvider } from "../definition/index.js";
15
+ import { VennFormatter } from "../formatting/index.js";
16
+ import { VennHoverProvider } from "../hover/index.js";
17
+ import { VennDocumentHighlightProvider, VennReferencesProvider } from "../references/index.js";
18
+ import { VennRenameProvider } from "../rename/index.js";
19
+ import { VennSemanticTokenProvider } from "../semantic/index.js";
20
+ import { VennSignatureHelpProvider } from "../signature/index.js";
21
+ import { VennDocumentSymbolProvider } from "../symbols/index.js";
22
+ import { createTypeService, warmTypes } from "../types/index.js";
23
+ import { registerVennChecks } from "../validation/index.js";
24
+ import { createImportResolver } from "../workspace/index.js";
25
+ import type { VennAddedServices, VennServices } from "./lsp.types.js";
26
+
27
+ const VennModule: Module<VennServices, PartialLangiumServices & VennAddedServices> = {
28
+ catalog: () => buildCatalog(allPlugins),
29
+ imports: () => createImportResolver(),
30
+ types: (services) => createTypeService(services),
31
+ parser: { Lexer: (services) => new VennLexer(services) },
32
+ lsp: {
33
+ SemanticTokenProvider: (services) => new VennSemanticTokenProvider(services),
34
+ HoverProvider: (services) => new VennHoverProvider(services),
35
+ DefinitionProvider: (services) => new VennDefinitionProvider(services),
36
+ CompletionProvider: (services) => new VennCompletionProvider(services),
37
+ DocumentSymbolProvider: () => new VennDocumentSymbolProvider(),
38
+ RenameProvider: (services) => new VennRenameProvider(services),
39
+ ReferencesProvider: (services) => new VennReferencesProvider(services),
40
+ DocumentHighlightProvider: () => new VennDocumentHighlightProvider(),
41
+ CodeActionProvider: (services) => new VennCodeActionProvider(services),
42
+ SignatureHelp: (services) => new VennSignatureHelpProvider(services),
43
+ Formatter: (services) => new VennFormatter(services),
44
+ },
45
+ };
46
+
47
+ /**
48
+ * Build the Venn language services on top of Langium's LSP stack. Registering
49
+ * the validator and warming the type cache happen here, so a caller only has to
50
+ * supply a connection and start the server.
51
+ *
52
+ * @param context Langium's shared module context: a connection and a file system.
53
+ * @returns The shared Langium services, and the Venn services built on them.
54
+ */
55
+ export function createVennLspServices(context: DefaultSharedModuleContext): {
56
+ shared: LangiumSharedServices;
57
+ Venn: VennServices;
58
+ } {
59
+ const shared = inject(createDefaultSharedModule(context), VennGeneratedSharedModule);
60
+ const Venn = inject(createDefaultModule({ shared }), VennGeneratedModule, VennModule);
61
+ shared.ServiceRegistry.register(Venn);
62
+ registerVennChecks(Venn);
63
+ warmTypes(shared, Venn.types);
64
+ return { shared, Venn };
65
+ }
@@ -0,0 +1,82 @@
1
+ import {
2
+ type ActionCall,
3
+ type Expr,
4
+ isActionCall,
5
+ isMatcherClause,
6
+ type MatcherClause,
7
+ } from "@venn-lang/core";
8
+ import { AstUtils, type LangiumDocument } from "langium";
9
+
10
+ /** Anything written as a bare word followed by bare arguments. */
11
+ interface Bareword {
12
+ args?: readonly Expr[];
13
+ readonly $cstNode?: { offset: number };
14
+ }
15
+
16
+ /**
17
+ * The action call the cursor is writing, if it is writing one.
18
+ *
19
+ * Not found by walking up from the token under the cursor: the cursor is on
20
+ * whitespace here (`http.on ` is the whole point), and in this grammar the
21
+ * newline is a real token belonging to the document, not to the statement. So
22
+ * the call is found by where it starts instead: the one that begins on this
23
+ * line, before the cursor. A statement holds at most one, so there is no
24
+ * innermost to choose between.
25
+ */
26
+ export function enclosingCall(document: LangiumDocument, offset: number): ActionCall | undefined {
27
+ const root = document.parseResult?.value;
28
+ if (!root) return undefined;
29
+ const from = lineStart(document.textDocument.getText(), offset);
30
+ let found: ActionCall | undefined;
31
+ for (const node of AstUtils.streamAst(root)) {
32
+ const start = node.$cstNode?.offset;
33
+ if (isActionCall(node) && start !== undefined && start >= from && start < offset) found = node;
34
+ }
35
+ return found;
36
+ }
37
+
38
+ /** The same for `expect subject contains ▮`; a matcher takes bare arguments too. */
39
+ export function enclosingMatcher(
40
+ document: LangiumDocument,
41
+ offset: number,
42
+ ): MatcherClause | undefined {
43
+ const root = document.parseResult?.value;
44
+ if (!root) return undefined;
45
+ const from = lineStart(document.textDocument.getText(), offset);
46
+ let found: MatcherClause | undefined;
47
+ for (const node of AstUtils.streamAst(root)) {
48
+ const start = node.$cstNode?.offset;
49
+ if (isMatcherClause(node) && start !== undefined && start >= from && start < offset) {
50
+ found = node;
51
+ }
52
+ }
53
+ return found && offset > (found.$cstNode?.offset ?? 0) + found.name.length ? found : undefined;
54
+ }
55
+
56
+ function lineStart(text: string, offset: number): number {
57
+ return text.lastIndexOf("\n", Math.max(0, offset - 1)) + 1;
58
+ }
59
+
60
+ /**
61
+ * Which argument the cursor is on.
62
+ *
63
+ * An argument that ends before the cursor has been written; one that ends at it
64
+ * is still being typed. So `http.on api|` is on the first argument and
65
+ * `http.on api |` has moved to the second, which is what makes the highlight
66
+ * move as the space is pressed rather than a keystroke late.
67
+ */
68
+ export function activeArg(call: Bareword, offset: number): number {
69
+ let written = 0;
70
+ for (const arg of call.args ?? []) {
71
+ const end = arg.$cstNode?.end;
72
+ if (end === undefined || end >= offset) break;
73
+ written += 1;
74
+ }
75
+ return written;
76
+ }
77
+
78
+ /** Whether the cursor sits after the verb rather than still inside its name. */
79
+ export function pastTarget(call: ActionCall, offset: number): boolean {
80
+ const start = call.$cstNode?.offset ?? 0;
81
+ return offset > start + call.target.length;
82
+ }
@@ -0,0 +1,55 @@
1
+ import { dottedPath, type Expr, isActionCall, isLetStmt, type LetStmt } from "@venn-lang/core";
2
+ import { type AstNode, AstUtils, type LangiumDocument } from "langium";
3
+
4
+ /** A call written without brackets, wherever the language allows one. */
5
+ export interface BareCall {
6
+ /** The verb being called: `http.get`. */
7
+ target: string;
8
+ /** The bare arguments after it, in order. */
9
+ args: readonly Expr[];
10
+ /** Where the verb's own name ends; the cursor must be past it. */
11
+ after: number;
12
+ /** The node it was found on, for looking names up in scope. */
13
+ node: AstNode;
14
+ }
15
+
16
+ /**
17
+ * The bracket-less call the cursor is writing.
18
+ *
19
+ * Two nodes spell one thing. `http.get "u"` standing alone is an action call;
20
+ * bound with `const r = http.get "u"` it is a `let` whose value happens to name
21
+ * a verb, with the arguments hanging off the statement. The same words deserve
22
+ * the same help, so both are found here.
23
+ */
24
+ export function enclosingBareCall(document: LangiumDocument, offset: number): BareCall | undefined {
25
+ const root = document.parseResult?.value;
26
+ if (!root) return undefined;
27
+ const from = lineStart(document.textDocument.getText(), offset);
28
+ let found: BareCall | undefined;
29
+ for (const node of AstUtils.streamAst(root)) {
30
+ const start = node.$cstNode?.offset;
31
+ if (start === undefined || start < from || start >= offset) continue;
32
+ found = asBareCall(node) ?? found;
33
+ }
34
+ return found && offset > found.after ? found : undefined;
35
+ }
36
+
37
+ function asBareCall(node: unknown): BareCall | undefined {
38
+ if (isActionCall(node)) {
39
+ const start = node.$cstNode?.offset ?? 0;
40
+ return { target: node.target, args: node.args, after: start + node.target.length, node };
41
+ }
42
+ return isLetStmt(node) ? letCall(node) : undefined;
43
+ }
44
+
45
+ /** `const r = http.get "u" { … }`: the verb is the value, the args follow it. */
46
+ function letCall(node: LetStmt): BareCall | undefined {
47
+ const target = dottedPath(node.value);
48
+ const end = node.value.$cstNode?.end;
49
+ if (!target || end === undefined) return undefined;
50
+ return { target, args: node.args, after: end, node };
51
+ }
52
+
53
+ function lineStart(text: string, offset: number): number {
54
+ return text.lastIndexOf("\n", Math.max(0, offset - 1)) + 1;
55
+ }
@@ -0,0 +1,90 @@
1
+ import { PRELUDE_SPECS } from "@venn-lang/core";
2
+ import { type ActionDefinition, type ArgSpec, paramSpecs } from "@venn-lang/sdk";
3
+ import { type FnSpec, showSpec } from "@venn-lang/types";
4
+ import type { SymbolCatalog } from "../catalog/index.js";
5
+ import type { CallShape, ShownArg } from "./call-shape.types.js";
6
+
7
+ /**
8
+ * What `target` takes, as the editor describes it back.
9
+ *
10
+ * A verb people can call is a verb people can be told about, whether a plugin
11
+ * contributes it or the prelude does. Returns undefined only when nothing in
12
+ * the language answers to that name.
13
+ */
14
+ export function callShape(target: string, catalog: SymbolCatalog): CallShape | undefined {
15
+ const dot = target.indexOf(".");
16
+ if (dot < 0) return preludeShape(target);
17
+ const entry = catalog.action(target.slice(0, dot), target.slice(dot + 1));
18
+ if (!entry) return undefined;
19
+ return {
20
+ target,
21
+ args: entry.action.args ? shownArgs(entry.action.args) : unnamed(entry.action.signature),
22
+ options: paramSpecs(entry.action.params),
23
+ doc: entry.action.doc,
24
+ returns: returnOf(entry.action),
25
+ };
26
+ }
27
+
28
+ /** The same, for a bareword matcher: `expect res contains "ok"`. */
29
+ export function matcherShape(name: string, catalog: SymbolCatalog): CallShape | undefined {
30
+ const entry = catalog.matcher(name);
31
+ if (!entry) return undefined;
32
+ return {
33
+ target: name,
34
+ args: shownArgs(entry.matcher.args ?? []),
35
+ options: paramSpecs(entry.matcher.params),
36
+ doc: entry.matcher.appliesTo ? `Applies to ${entry.matcher.appliesTo}.` : undefined,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * What the verb gives back, in the language's own words.
42
+ *
43
+ * Read from the declared type, never from prose beside it: a second place to
44
+ * say the same thing is a second place to say it differently.
45
+ */
46
+ function returnOf(action: ActionDefinition): string | undefined {
47
+ const result = action.signature?.result;
48
+ // Nothing worth saying about a verb that answers nothing.
49
+ if (!result || (result.kind === "prim" && result.name === "void")) return undefined;
50
+ return showSpec(result);
51
+ }
52
+
53
+ /** Published argument specs as the editor shows them. */
54
+ export function shownArgs(specs: readonly ArgSpec[]): readonly ShownArg[] {
55
+ return specs.map(shownArg);
56
+ }
57
+
58
+ /**
59
+ * What can still be said about a verb whose author named nothing: how many
60
+ * arguments it takes, and of what. Worse than a name and far better than
61
+ * silence, and the only thing a plugin from outside this repo may ever offer.
62
+ */
63
+ function unnamed(signature: FnSpec | undefined): readonly ShownArg[] {
64
+ return (signature?.params ?? []).map((type) => ({ name: "", type: showSpec(type) }));
65
+ }
66
+
67
+ function shownArg(spec: ArgSpec): ShownArg {
68
+ return {
69
+ name: spec.name,
70
+ type: showSpec(spec.type),
71
+ doc: spec.doc,
72
+ optional: spec.optional,
73
+ rest: spec.rest,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * The prelude describes itself already: the checker reads its types and the
79
+ * editor its prose. This reads the same table, so the two never disagree.
80
+ */
81
+ function preludeShape(name: string): CallShape | undefined {
82
+ const spec = PRELUDE_SPECS[name];
83
+ if (!spec) return undefined;
84
+ return {
85
+ target: name,
86
+ args: (spec.args ?? []).map((each) => ({ ...each })),
87
+ options: [],
88
+ doc: spec.doc,
89
+ };
90
+ }
@@ -0,0 +1,34 @@
1
+ import type { ParamSpec } from "@venn-lang/sdk";
2
+
3
+ /**
4
+ * A call as the editor describes it back to whoever is typing it.
5
+ *
6
+ * Deliberately not an LSP type: the same shape answers signature help, hover
7
+ * and the completion detail line, and only one of those speaks LSP.
8
+ */
9
+ export interface CallShape {
10
+ /** What is being called: `http.on`, `print`. */
11
+ target: string;
12
+ args: readonly ShownArg[];
13
+ /**
14
+ * The keys the trailing `{ … }` accepts, empty when it accepts none.
15
+ *
16
+ * The options are an argument like any other, the last one and a map, so they
17
+ * are described here rather than flagged with a boolean. A reader halfway
18
+ * through `http.get(url, ` is asking exactly what may go next.
19
+ */
20
+ options: readonly ParamSpec[];
21
+ doc?: string;
22
+ /** Prose for what comes back, when the author wrote any. */
23
+ returns?: string;
24
+ }
25
+
26
+ export interface ShownArg {
27
+ name: string;
28
+ /** The type as text: `string`, `HttpServer`, `fn(request) -> reply`. */
29
+ type: string;
30
+ doc?: string;
31
+ optional?: boolean;
32
+ /** Takes this one and every argument after it. */
33
+ rest?: boolean;
34
+ }
@@ -0,0 +1,52 @@
1
+ import {
2
+ type AstNode,
3
+ DYNAMIC,
4
+ isDatasetDecl,
5
+ isFnDecl,
6
+ isFnExpr,
7
+ isFragmentDecl,
8
+ isLetStmt,
9
+ type ParamList,
10
+ showTypes,
11
+ } from "@venn-lang/core";
12
+ import type { LangiumDocument } from "langium";
13
+ import type { TypeService } from "../types/index.js";
14
+ import type { CallShape } from "./call-shape.types.js";
15
+
16
+ export interface DeclaredShapeArgs {
17
+ /** The node that binds the name: what `findBinding` answered. */
18
+ binding: AstNode;
19
+ name: string;
20
+ document: LangiumDocument;
21
+ types: TypeService;
22
+ }
23
+
24
+ /**
25
+ * What a function the file itself declares takes, by name.
26
+ *
27
+ * The names are written right there in the source; the types come from
28
+ * inference, which for an unannotated `fn` means a variable until the body
29
+ * pins it down. A name with `a` next to it still tells the reader which
30
+ * argument they are on, which is the question a half-typed call asks.
31
+ */
32
+ export function declaredShape(args: DeclaredShapeArgs): CallShape | undefined {
33
+ const params = paramsOf(args.binding);
34
+ if (!params) return undefined;
35
+ const known = args.types.of(args.document).types;
36
+ // Named together, so two unrelated parameters do not both come back as `a`.
37
+ const shown = showTypes(params.params.map((param) => known.get(param) ?? DYNAMIC));
38
+ return {
39
+ target: args.name,
40
+ args: params.params.map((param, at) => ({ name: param.name, type: shown[at] ?? "dynamic" })),
41
+ options: [],
42
+ returns: undefined,
43
+ };
44
+ }
45
+
46
+ /** The parameter list, wherever the binding keeps one. */
47
+ function paramsOf(binding: AstNode): ParamList | undefined {
48
+ if (isFnDecl(binding) || isFragmentDecl(binding) || isFnExpr(binding)) return binding.params;
49
+ // `const op = (a, b) => a + b`: the parameters belong to the lambda it holds.
50
+ const value = (isLetStmt(binding) || isDatasetDecl(binding)) && binding.value;
51
+ return value && isFnExpr(value) ? value.params : undefined;
52
+ }
@@ -0,0 +1,11 @@
1
+ export { activeArg, enclosingCall, enclosingMatcher, pastTarget } from "./action-call.js";
2
+ export type { BareCall } from "./bare-call.js";
3
+ export { enclosingBareCall } from "./bare-call.js";
4
+ export { callShape, matcherShape, shownArgs } from "./call-shape.js";
5
+ export type { CallShape, ShownArg } from "./call-shape.types.js";
6
+ export { declaredShape } from "./declared-shape.js";
7
+ export type { ParenCall } from "./paren-call.js";
8
+ export { enclosingParenCall } from "./paren-call.js";
9
+ export { bracketed, signatureOfShape } from "./render-shape.js";
10
+ export { shapeAt } from "./shape-at.js";
11
+ export { VennSignatureHelpProvider } from "./signature-help.js";
@@ -0,0 +1,77 @@
1
+ import type { AstNode } from "@venn-lang/core";
2
+ import { CstUtils, type LangiumDocument } from "langium";
3
+
4
+ /** A bracketed call the cursor is inside, read from the text. */
5
+ export interface ParenCall {
6
+ /** The dotted name being called: `saudacao`, `fmt.json`. */
7
+ path: string;
8
+ /** Which argument is being written, counted by the commas before it. */
9
+ active: number;
10
+ /** A node in the same scope, for looking the name up. */
11
+ host: AstNode | undefined;
12
+ }
13
+
14
+ /**
15
+ * The innermost `f(…)` whose brackets hold the cursor.
16
+ *
17
+ * Read from the text rather than from the tree, because the states that most
18
+ * need explaining are exactly the ones that do not parse: `f("a", ▮)` has a
19
+ * trailing comma, which is a syntax error, and a hint that disappears the
20
+ * moment you press comma is a hint that is never there when wanted.
21
+ */
22
+ export function enclosingParenCall(
23
+ document: LangiumDocument,
24
+ offset: number,
25
+ ): ParenCall | undefined {
26
+ const text = document.textDocument.getText();
27
+ const open = innermostOpenParen(text, offset);
28
+ if (open < 0) return undefined;
29
+ const path = nameBefore(text.slice(0, open));
30
+ if (!path) return undefined;
31
+ return { path, active: commas(text.slice(open + 1, offset)), host: nodeNear(document, open) };
32
+ }
33
+
34
+ /** How far back to look for the bracket. A call this long is not being typed. */
35
+ const REACH = 2000;
36
+
37
+ /** The `(` still open at the cursor, scanning backwards. */
38
+ function innermostOpenParen(text: string, offset: number): number {
39
+ let depth = 0;
40
+ for (let at = offset - 1; at >= Math.max(0, offset - REACH); at -= 1) {
41
+ const char = text[at];
42
+ if (char === ")") depth += 1;
43
+ else if (char === "(") {
44
+ if (depth === 0) return at;
45
+ depth -= 1;
46
+ }
47
+ }
48
+ return -1;
49
+ }
50
+
51
+ const CALLEE = /([A-Za-z_]\w*(?:\.\w+)*)$/;
52
+
53
+ /** The dotted name written immediately before the bracket. */
54
+ function nameBefore(before: string): string | undefined {
55
+ return CALLEE.exec(before)?.[1];
56
+ }
57
+
58
+ /** Commas at this bracket's own level: those in a nested call are not ours. */
59
+ function commas(inside: string): number {
60
+ let depth = 0;
61
+ let count = 0;
62
+ for (const char of inside) {
63
+ if (char === "(" || char === "[" || char === "{") depth += 1;
64
+ else if (char === ")" || char === "]" || char === "}") depth -= 1;
65
+ else if (char === "," && depth === 0) count += 1;
66
+ }
67
+ return count;
68
+ }
69
+
70
+ /** Any node at the bracket, so a name can be looked up in the right scope. */
71
+ function nodeNear(document: LangiumDocument, offset: number): AstNode | undefined {
72
+ const root = document.parseResult?.value?.$cstNode;
73
+ if (!root) return undefined;
74
+ const leaf =
75
+ CstUtils.findLeafNodeAtOffset(root, offset) ?? CstUtils.findLeafNodeBeforeOffset(root, offset);
76
+ return leaf?.astNode;
77
+ }