@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,88 @@
1
+ import type { EnvVar } from "./env.types.js";
2
+
3
+ /**
4
+ * Words that mean the value is a credential. Matched per segment, never as a
5
+ * substring: `KEYCLOAK_URL` contains "key" and is just a URL.
6
+ */
7
+ const SECRET_WORDS = new Set([
8
+ "pass",
9
+ "passwd",
10
+ "password",
11
+ "secret",
12
+ "token",
13
+ "key",
14
+ "apikey",
15
+ "credential",
16
+ "credentials",
17
+ "auth",
18
+ ]);
19
+
20
+ const REDACTED = "‹redacted›";
21
+
22
+ /**
23
+ * True when the name reads like a credential, so its value is never shown.
24
+ *
25
+ * A hover that prints a password during a screen share leaks it, so redaction
26
+ * belongs at the producer (§16), which is here, not in whatever renders the
27
+ * tooltip.
28
+ */
29
+ export function isSecretName(name: string): boolean {
30
+ return segmentsOf(name).some((segment) => SECRET_WORDS.has(segment));
31
+ }
32
+
33
+ /** `ADMIN_PASSWORD` and `adminPassword` both split into `admin`, `password`. */
34
+ function segmentsOf(name: string): string[] {
35
+ return name
36
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
37
+ .toLowerCase()
38
+ .split(/[^a-z0-9]+/)
39
+ .filter(Boolean);
40
+ }
41
+
42
+ /**
43
+ * Flatten `[env.local]`, `[env.ci]`… into one declared variable per name, each
44
+ * carrying the comment written above it. `venn.toml` is where an environment
45
+ * variable is documented, the same way `##` documents a declaration.
46
+ */
47
+ export function envVars(
48
+ sections: Record<string, Record<string, string>>,
49
+ docs: Record<string, string> = {},
50
+ ): EnvVar[] {
51
+ const found = new Map<string, EnvVar>();
52
+ for (const [environment, vars] of Object.entries(sections)) {
53
+ for (const [name, value] of Object.entries(vars)) {
54
+ record({ found, name, environment, value, doc: docs[name] });
55
+ }
56
+ }
57
+ const declared = [...found.values()];
58
+ return declared.length > 0 ? [selectedEnvironment(sections), ...declared] : declared;
59
+ }
60
+
61
+ /** `env.name` is not in the file: the runner sets it to whichever `--env` ran. */
62
+ function selectedEnvironment(sections: Record<string, Record<string, string>>): EnvVar {
63
+ return {
64
+ name: "name",
65
+ doc: "The environment this run selected, from `--env`.",
66
+ secret: false,
67
+ values: Object.keys(sections).map((environment) => ({ environment, value: environment })),
68
+ };
69
+ }
70
+
71
+ function record(args: {
72
+ found: Map<string, EnvVar>;
73
+ name: string;
74
+ environment: string;
75
+ value: string;
76
+ doc?: string;
77
+ }): void {
78
+ const secret = isSecretName(args.name);
79
+ const entry = { environment: args.environment, value: secret ? REDACTED : args.value };
80
+ const existing = args.found.get(args.name);
81
+ if (existing) existing.values.push(entry);
82
+ else args.found.set(args.name, { name: args.name, doc: args.doc, secret, values: [entry] });
83
+ }
84
+
85
+ /** Just the declared names: what the checker matches reads against. */
86
+ export function envNames(sections: Record<string, Record<string, string>>): string[] {
87
+ return envVars(sections).map((variable) => variable.name);
88
+ }
@@ -0,0 +1,9 @@
1
+ /** One variable declared in `venn.toml`, with what each environment sets it to. */
2
+ export interface EnvVar {
3
+ name: string;
4
+ /** The comment written above the key in `venn.toml`. */
5
+ doc?: string;
6
+ /** True when the name reads like a credential, so its value is never shown. */
7
+ secret: boolean;
8
+ values: { environment: string; value: string }[];
9
+ }
@@ -0,0 +1,3 @@
1
+ export type { EnvVar } from "./env.types.js";
2
+ export { envNames, envVars, isSecretName } from "./env-vars.js";
3
+ export { envHover } from "./render-env.js";
@@ -0,0 +1,25 @@
1
+ import { code, fence, labelled, rule, sections } from "../markdown/index.js";
2
+ import type { EnvVar } from "./env.types.js";
3
+
4
+ /**
5
+ * Hover for `env.NAME`: what it is for, then what it is set to per environment.
6
+ * Secrets show that they exist without showing what they are.
7
+ */
8
+ export function envHover(variable: EnvVar): string {
9
+ return rule([
10
+ fence(`env.${variable.name}`),
11
+ sections([variable.doc, valuesBlock(variable), secretNote(variable)]),
12
+ "**Declared in** `venn.toml`",
13
+ ]);
14
+ }
15
+
16
+ function valuesBlock(variable: EnvVar): string {
17
+ const values = variable.values
18
+ .map((entry) => `- ${code(entry.environment)}: ${code(entry.value)}`)
19
+ .join("\n");
20
+ return labelled("Value", values);
21
+ }
22
+
23
+ function secretNote(variable: EnvVar): string | undefined {
24
+ return variable.secret ? "Reads like a credential, so the value is never printed." : undefined;
25
+ }
@@ -0,0 +1,68 @@
1
+ import { formatOptionsFrom, formatText } from "@venn-lang/core";
2
+ import type { LangiumDocument } from "langium";
3
+ import type { Formatter } from "langium/lsp";
4
+ import type {
5
+ DocumentFormattingParams,
6
+ DocumentOnTypeFormattingOptions,
7
+ DocumentOnTypeFormattingParams,
8
+ DocumentRangeFormattingParams,
9
+ FormattingOptions,
10
+ Range,
11
+ TextEdit,
12
+ } from "vscode-languageserver";
13
+ import type { VennServices } from "../services/lsp.types.js";
14
+ import type { ImportResolver } from "../workspace/index.js";
15
+
16
+ /**
17
+ * Formatting runs through `@venn-lang/core`, so the editor and `venn fmt` produce
18
+ * byte-identical output. Project settings come from `[format]` in `venn.toml`;
19
+ * the editor's own indent settings fill in what the project left unset.
20
+ */
21
+ export class VennFormatter implements Formatter {
22
+ private readonly imports: ImportResolver;
23
+
24
+ constructor(services: VennServices) {
25
+ this.imports = services.imports;
26
+ }
27
+
28
+ /** Re-indent the line as soon as a block is closed. */
29
+ get formatOnTypeOptions(): DocumentOnTypeFormattingOptions {
30
+ return { firstTriggerCharacter: "}", moreTriggerCharacter: ["\n"] };
31
+ }
32
+
33
+ formatDocument(document: LangiumDocument, params: DocumentFormattingParams): TextEdit[] {
34
+ return this.rewrite(document, params.options);
35
+ }
36
+
37
+ formatDocumentRange(
38
+ document: LangiumDocument,
39
+ params: DocumentRangeFormattingParams,
40
+ ): TextEdit[] {
41
+ return this.rewrite(document, params.options);
42
+ }
43
+
44
+ formatDocumentOnType(
45
+ document: LangiumDocument,
46
+ params: DocumentOnTypeFormattingParams,
47
+ ): TextEdit[] {
48
+ return this.rewrite(document, params.options);
49
+ }
50
+
51
+ // One whole-document edit: the header may move lines, so per-line edits cannot
52
+ // express the result.
53
+ private rewrite(document: LangiumDocument, editor: FormattingOptions): TextEdit[] {
54
+ const source = document.textDocument.getText();
55
+ const project = formatOptionsFrom(this.imports.formatSettings(document.uri));
56
+ const formatted = formatText(source, { ...fromEditor(editor), ...project });
57
+ return formatted === source ? [] : [{ range: whole(document), newText: formatted }];
58
+ }
59
+ }
60
+
61
+ function fromEditor(options: FormattingOptions): { indentWidth: number; useTabs: boolean } {
62
+ return { indentWidth: options.tabSize, useTabs: !options.insertSpaces };
63
+ }
64
+
65
+ function whole(document: LangiumDocument): Range {
66
+ const text = document.textDocument;
67
+ return { start: { line: 0, character: 0 }, end: text.positionAt(text.getText().length) };
68
+ }
@@ -0,0 +1 @@
1
+ export { VennFormatter } from "./formatter.js";
@@ -0,0 +1,322 @@
1
+ import {
2
+ type ActionCall,
3
+ type AstNode,
4
+ type Document,
5
+ isActionCall,
6
+ isAnnotation,
7
+ isDecoDecl,
8
+ isFnDecl,
9
+ isFragmentDecl,
10
+ isMatcherClause,
11
+ isMember,
12
+ isNamedType,
13
+ isRunStmt,
14
+ isUseDecl,
15
+ isValueImport,
16
+ type ValueImport,
17
+ } from "@venn-lang/core";
18
+ import {
19
+ type CstNode,
20
+ CstUtils,
21
+ GrammarUtils,
22
+ type LangiumDocument,
23
+ type LangiumDocuments,
24
+ } from "langium";
25
+ import type { HoverProvider } from "langium/lsp";
26
+ import { type Hover, type HoverParams, MarkupKind } from "vscode-languageserver";
27
+ import type { SymbolCatalog } from "../catalog/index.js";
28
+ import { declaredDeco, decoHover, decoNamed, documentedHover } from "../deco/index.js";
29
+ import {
30
+ findBinding,
31
+ hostAt,
32
+ interpolationAt,
33
+ pathOf,
34
+ resolveFragment,
35
+ resolveImported,
36
+ } from "../document/index.js";
37
+ import { waitingFns } from "../effects/index.js";
38
+ import { envHover, envVars } from "../env/index.js";
39
+ import type { VennServices } from "../services/lsp.types.js";
40
+ import type { TypeService } from "../types/index.js";
41
+ import type { ImportResolver } from "../workspace/index.js";
42
+ import { keywordHover } from "./keywords.js";
43
+ import { actionHover, matcherHover } from "./render-action.js";
44
+ import { declarationHover, fragmentHover, useHover } from "./render-decl.js";
45
+ import { importedHover, importPathHover } from "./render-imported.js";
46
+ import { memberHover } from "./render-symbol.js";
47
+ import { typeNameHover } from "./render-type-name.js";
48
+ import { resolveSymbol } from "./resolve-symbol.js";
49
+ import { inferredType } from "./type-hover.js";
50
+
51
+ /**
52
+ * Rich markdown hover: actions, matchers, fragments, bindings and keywords.
53
+ *
54
+ * Which token the cursor sits on decides the answer, so every branch checks the
55
+ * CST property it belongs to rather than the enclosing node alone.
56
+ */
57
+ export class VennHoverProvider implements HoverProvider {
58
+ private readonly catalog: SymbolCatalog;
59
+ private readonly documents: LangiumDocuments;
60
+ private readonly imports: ImportResolver;
61
+ private readonly types: TypeService;
62
+
63
+ constructor(services: VennServices) {
64
+ this.catalog = services.catalog;
65
+ this.documents = services.shared.workspace.LangiumDocuments;
66
+ this.imports = services.imports;
67
+ this.types = services.types;
68
+ }
69
+
70
+ async getHoverContent(
71
+ document: LangiumDocument,
72
+ params: HoverParams,
73
+ ): Promise<Hover | undefined> {
74
+ const offset = document.textDocument.offsetAt(params.position);
75
+ const leaf = leafAt(document, offset);
76
+ const value =
77
+ this.inString(document, offset) ??
78
+ (leaf && (await this.describe(leaf, document))) ??
79
+ (leaf ? inferredType(leaf, this.types) : undefined);
80
+ return value ? { contents: { kind: MarkupKind.Markdown, value } } : undefined;
81
+ }
82
+
83
+ /** Inside `"${…}"` the name is code, so it hovers like code, not like text. */
84
+ private inString(document: LangiumDocument, offset: number): string | undefined {
85
+ const hit = interpolationAt({ document, offset });
86
+ if (!hit) return undefined;
87
+ if (hit.path.startsWith("env.")) {
88
+ // `env` the namespace explains itself; `env.NAME` describes the variable.
89
+ return hit.isHead ? keywordHover("env") : this.envVar(hit.name, document);
90
+ }
91
+ // Resolve against the parsed expression when there is one: a name bound
92
+ // inside the placeholder, such as a lambda parameter, lives only there.
93
+ const host = hostAt({ document, offset, types: this.types.of(document) }) ?? hit.host;
94
+ return (
95
+ this.member(host, hit.name, document) ??
96
+ this.resolve({ path: hit.path, segment: hit.segment, host, document })
97
+ );
98
+ }
99
+
100
+ /**
101
+ * A member read off the node rather than off the text.
102
+ *
103
+ * The dotted-path resolver can only name a chain of identifiers, so it has
104
+ * nothing to say about `(1234.567).round` or `f(x).len`. The receiver's
105
+ * inferred type is right there on the tree, so read it from there instead.
106
+ */
107
+ private member(node: AstNode, name: string, document: LangiumDocument): string | undefined {
108
+ if (!isMember(node) || node.member !== name) return undefined;
109
+ const receiver = this.types.of(document).types.get(node.receiver);
110
+ return receiver ? memberHover({ receiver, member: name }) : undefined;
111
+ }
112
+
113
+ /** The one resolver every place uses, so a name reads the same anywhere. */
114
+ private resolve(args: {
115
+ path: string;
116
+ segment: number;
117
+ host: AstNode;
118
+ document: LangiumDocument;
119
+ }): string | undefined {
120
+ return resolveSymbol({ ...args, catalog: this.catalog, types: this.types });
121
+ }
122
+
123
+ /** Which of this file's functions wait for something, for the signature to say so. */
124
+ private waits(document: LangiumDocument): ReadonlySet<string> {
125
+ const root = document.parseResult?.value as Document | undefined;
126
+ return root ? waitingFns(root, this.catalog) : new Set();
127
+ }
128
+
129
+ /** `env.NAME` is not a variable anyone declared here; `venn.toml` declares it. */
130
+ private envVar(name: string, document: LangiumDocument): string | undefined {
131
+ const found = envVars(this.imports.env(document.uri), this.imports.envDocs(document.uri)).find(
132
+ (variable) => variable.name === name,
133
+ );
134
+ return found && envHover(found);
135
+ }
136
+
137
+ private async describe(leaf: CstNode, document: LangiumDocument): Promise<string | undefined> {
138
+ return (await this.symbol(leaf, document)) ?? keywordHover(leaf.text);
139
+ }
140
+
141
+ // Which token the cursor sits on decides this: `run login()` is a keyword on
142
+ // `run` and a fragment on `login`.
143
+ private async symbol(leaf: CstNode, document: LangiumDocument): Promise<string | undefined> {
144
+ const node = leaf.astNode;
145
+ if (isAnnotation(node)) return this.decorator(node.name, document);
146
+ const env = envPath(node);
147
+ if (env) return this.envVar(env, document);
148
+ if (isValueImport(node)) return this.imported(node, leaf, document);
149
+ const symbol = this.pathHover(node, document);
150
+ if (symbol) return symbol;
151
+ const member = this.member(node, leaf.text, document);
152
+ if (member) return member;
153
+ if (isActionCall(node) && on(leaf, node, "target"))
154
+ return this.callTarget(node, leaf, document);
155
+ if (isNamedType(node)) return typeNameHover(node.name, this.catalog);
156
+ if (isMatcherClause(node) && on(leaf, node, "name"))
157
+ return matcherHover(node.name, this.catalog);
158
+ if (isUseDecl(node) && on(leaf, node, "pkg")) return useHover(node, this.catalog);
159
+ return this.declared(leaf, node, document);
160
+ }
161
+
162
+ /**
163
+ * A name in an `import` list, or the specifier it comes from.
164
+ *
165
+ * The list is the one place a name is neither declared nor used, so no other
166
+ * branch reaches it. It is also where a reader is deciding whether they want
167
+ * the thing at all, which is where a hover is worth the most.
168
+ */
169
+ private async imported(
170
+ node: ValueImport,
171
+ leaf: CstNode,
172
+ document: LangiumDocument,
173
+ ): Promise<string | undefined> {
174
+ const name = importedName(node, leaf.text);
175
+ const location = await resolveImported({
176
+ name: name ?? "",
177
+ decl: node,
178
+ document,
179
+ documents: this.documents,
180
+ imports: this.imports,
181
+ });
182
+ if (name) return importedHover({ location, name, types: this.types });
183
+ // The specifier, and `helpers` in `import * as helpers`, which names the
184
+ // whole file rather than anything inside it, so the file is the answer.
185
+ if (!on(leaf, node, "path") && leaf.text !== node.wildcard) return undefined;
186
+ return importPathHover({ location, path: node.path });
187
+ }
188
+
189
+ /**
190
+ * The verb of a call written as a statement.
191
+ *
192
+ * Usually a plugin's, as in `http.get`. But `target.wrap(…)` is a method on a
193
+ * value, and the parser reads it as a call whose target is text, so the
194
+ * member chain a hover normally walks does not exist. Resolving from that
195
+ * text puts it back, which is why hovering `target` inside a `deco` answers.
196
+ */
197
+ private callTarget(
198
+ node: ActionCall,
199
+ leaf: CstNode,
200
+ document: LangiumDocument,
201
+ ): string | undefined {
202
+ const head = node.target.split(".")[0];
203
+ if (head && findBinding(node, head)) {
204
+ return this.resolve({
205
+ path: node.target,
206
+ segment: dotsBefore(node, leaf),
207
+ host: node,
208
+ document,
209
+ });
210
+ }
211
+ return actionHover(node.target, this.catalog);
212
+ }
213
+
214
+ /**
215
+ * A dotted path written plainly: `fmt.json`, `xs.map`, `range`. The segment
216
+ * under the cursor decides what is described: the namespace, or its verb.
217
+ */
218
+ private pathHover(node: AstNode, document: LangiumDocument): string | undefined {
219
+ const path = pathOf(outermost(node));
220
+ if (!path) return undefined;
221
+ return this.resolve({ path, segment: segmentAt(node), host: node, document });
222
+ }
223
+
224
+ private async declared(
225
+ leaf: CstNode,
226
+ node: AstNode,
227
+ document: LangiumDocument,
228
+ ): Promise<string | undefined> {
229
+ if (isRunStmt(node) && on(leaf, node, "target")) return this.fragment(node.target, document);
230
+ if (isFnDecl(node) && on(leaf, node, "name"))
231
+ return declarationHover({ document, node, types: this.types, waiting: this.waits(document) });
232
+ if (isFragmentDecl(node) && on(leaf, node, "name")) return this.fragment(node.name, document);
233
+ if (isDecoDecl(node) && on(leaf, node, "name")) {
234
+ return decoHover(declaredDeco({ decl: node, document }));
235
+ }
236
+ if (!on(leaf, node, "name")) return undefined;
237
+ return declarationHover({ document, node, types: this.types, waiting: this.waits(document) });
238
+ }
239
+
240
+ /**
241
+ * `@name`: whatever defines it. A `deco` this file declares or imported, or
242
+ * a built-in; either way the same text a hover on the `deco` itself gives.
243
+ */
244
+ private async decorator(name: string, document: LangiumDocument): Promise<string | undefined> {
245
+ const scope = { document, documents: this.documents, imports: this.imports };
246
+ const found = await decoNamed(name, scope);
247
+ return found ? decoHover(found) : documentedHover(name);
248
+ }
249
+
250
+ private async fragment(name: string, document: LangiumDocument): Promise<string | undefined> {
251
+ const location = await resolveFragment({
252
+ name,
253
+ document,
254
+ documents: this.documents,
255
+ imports: this.imports,
256
+ });
257
+ return location && fragmentHover(location);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Which name of an import the cursor is on, if any.
263
+ *
264
+ * `import { a, b }` names two things and `import x from` names one, but both
265
+ * are one node carrying plain strings, so the word under the cursor is the
266
+ * only thing that says which was meant.
267
+ */
268
+ function importedName(node: ValueImport, text: string): string | undefined {
269
+ if (node.names.includes(text)) return text;
270
+ return node.default === text ? text : undefined;
271
+ }
272
+
273
+ /** True when the cursor's token lies inside the CST range of that property. */
274
+ function on(leaf: CstNode, node: AstNode, property: string): boolean {
275
+ const cst = GrammarUtils.findNodeForProperty(node.$cstNode, property);
276
+ return Boolean(cst && leaf.offset >= cst.offset && leaf.offset < cst.offset + cst.length);
277
+ }
278
+
279
+ function leafAt(document: LangiumDocument, offset: number): CstNode | undefined {
280
+ const root = document.parseResult?.value?.$cstNode;
281
+ return root ? CstUtils.findLeafNodeAtOffset(root, offset) : undefined;
282
+ }
283
+
284
+ /** The variable name in `env.NAME`, when that is what the cursor is on. */
285
+ function envPath(node: AstNode): string | undefined {
286
+ const path = pathOf(node);
287
+ return path?.startsWith("env.") && !path.slice(4).includes(".") ? path.slice(4) : undefined;
288
+ }
289
+
290
+ /** Climb to the end of a member chain: from `fmt` in `fmt.json`, to `fmt.json`. */
291
+ function outermost(node: AstNode): AstNode {
292
+ let current = node;
293
+ while (isMember(current.$container) && current.$container.receiver === current) {
294
+ current = current.$container;
295
+ }
296
+ return current;
297
+ }
298
+
299
+ /**
300
+ * The same count for a call whose target is text rather than a chain: the dots
301
+ * standing between the start of `target.wrap` and the token under the cursor.
302
+ */
303
+ function dotsBefore(node: ActionCall, leaf: CstNode): number {
304
+ const start = node.$cstNode?.offset ?? 0;
305
+ const upTo = node.target.slice(0, Math.max(0, leaf.offset - start));
306
+ return upTo.split(".").length - 1;
307
+ }
308
+
309
+ /**
310
+ * How many dots separate the chain's head from the token under the cursor. The
311
+ * cursor's own node is the innermost one that reaches it, so its depth in the
312
+ * chain is the answer: `a` is 0, `a.b` is 1, `a.b.c` is 2.
313
+ */
314
+ function segmentAt(node: AstNode): number {
315
+ let depth = 0;
316
+ let current: AstNode = node;
317
+ while (isMember(current)) {
318
+ depth += 1;
319
+ current = current.receiver;
320
+ }
321
+ return depth;
322
+ }
@@ -0,0 +1 @@
1
+ export { VennHoverProvider } from "./hover.js";