@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.
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/bin/venn-lsp.d.mts +1 -0
- package/dist/bin/venn-lsp.mjs +17 -0
- package/dist/bin/venn-lsp.mjs.map +1 -0
- package/dist/index.d.mts +722 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/server-BET60xwK.mjs +5376 -0
- package/dist/server-BET60xwK.mjs.map +1 -0
- package/package.json +65 -0
- package/src/bin/venn-lsp.ts +17 -0
- package/src/catalog/build-catalog.ts +72 -0
- package/src/catalog/catalog.types.ts +44 -0
- package/src/catalog/index.ts +2 -0
- package/src/code-actions/anchor.ts +49 -0
- package/src/code-actions/code-actions.ts +139 -0
- package/src/code-actions/exporting-modules.ts +73 -0
- package/src/code-actions/index.ts +5 -0
- package/src/completion/argument-items.ts +81 -0
- package/src/completion/completion.ts +416 -0
- package/src/completion/completion.types.ts +38 -0
- package/src/completion/context.ts +144 -0
- package/src/completion/icons.ts +83 -0
- package/src/completion/index.ts +4 -0
- package/src/completion/items.ts +232 -0
- package/src/completion/member-items.ts +147 -0
- package/src/completion/module-paths.ts +63 -0
- package/src/completion/options-map-items.ts +47 -0
- package/src/completion/read-from.ts +77 -0
- package/src/completion/type-name-items.ts +62 -0
- package/src/deco/builtin-decos.ts +35 -0
- package/src/deco/builtin-docs.ts +26 -0
- package/src/deco/declared-deco.ts +56 -0
- package/src/deco/deco.types.ts +23 -0
- package/src/deco/decos-in-scope.ts +63 -0
- package/src/deco/imported-decos.ts +49 -0
- package/src/deco/index.ts +9 -0
- package/src/deco/kind-of.ts +15 -0
- package/src/deco/render-deco.ts +44 -0
- package/src/definition/definition.ts +93 -0
- package/src/definition/index.ts +1 -0
- package/src/docs/doc.types.ts +17 -0
- package/src/docs/index.ts +4 -0
- package/src/docs/parse-doc.ts +42 -0
- package/src/docs/read-doc.ts +45 -0
- package/src/docs/render-doc.ts +42 -0
- package/src/document/exported-names.ts +30 -0
- package/src/document/expression-path.ts +43 -0
- package/src/document/find-binding.ts +82 -0
- package/src/document/host-at.ts +61 -0
- package/src/document/imported-modules.ts +91 -0
- package/src/document/imported-names.ts +15 -0
- package/src/document/index.ts +19 -0
- package/src/document/interpolation-at.ts +134 -0
- package/src/document/names-in-scope.ts +126 -0
- package/src/document/resolve-fragment.ts +38 -0
- package/src/document/resolve-imported.ts +35 -0
- package/src/effects/index.ts +1 -0
- package/src/effects/waiting-fns.ts +83 -0
- package/src/env/env-vars.ts +88 -0
- package/src/env/env.types.ts +9 -0
- package/src/env/index.ts +3 -0
- package/src/env/render-env.ts +25 -0
- package/src/formatting/formatter.ts +68 -0
- package/src/formatting/index.ts +1 -0
- package/src/hover/hover.ts +322 -0
- package/src/hover/index.ts +1 -0
- package/src/hover/keywords.ts +168 -0
- package/src/hover/render-action.ts +94 -0
- package/src/hover/render-decl.ts +155 -0
- package/src/hover/render-imported.ts +49 -0
- package/src/hover/render-symbol.ts +108 -0
- package/src/hover/render-type-name.ts +78 -0
- package/src/hover/resolve-symbol.ts +77 -0
- package/src/hover/type-hover.ts +62 -0
- package/src/index.ts +38 -0
- package/src/markdown/index.ts +1 -0
- package/src/markdown/markdown.ts +38 -0
- package/src/references/document-highlight.ts +45 -0
- package/src/references/find-occurrences.ts +49 -0
- package/src/references/index.ts +13 -0
- package/src/references/name-property.ts +14 -0
- package/src/references/occurrence-range.ts +23 -0
- package/src/references/occurrences.ts +124 -0
- package/src/references/references.ts +38 -0
- package/src/references/symbol-at.ts +114 -0
- package/src/references/symbol.types.ts +38 -0
- package/src/rename/declares-deco.ts +15 -0
- package/src/rename/index.ts +1 -0
- package/src/rename/rename.ts +73 -0
- package/src/semantic/highlight-calls.ts +56 -0
- package/src/semantic/highlight-interpolation.ts +135 -0
- package/src/semantic/highlight-keywords.ts +115 -0
- package/src/semantic/highlight-literals.ts +38 -0
- package/src/semantic/highlight-module.ts +36 -0
- package/src/semantic/highlight-names.ts +97 -0
- package/src/semantic/highlight-paths.ts +80 -0
- package/src/semantic/highlight.types.ts +10 -0
- package/src/semantic/index.ts +1 -0
- package/src/semantic/semantic-tokens.ts +32 -0
- package/src/server/index.ts +1 -0
- package/src/server/start-server.ts +15 -0
- package/src/services/index.ts +2 -0
- package/src/services/lsp.types.ts +14 -0
- package/src/services/venn-lsp-module.ts +65 -0
- package/src/signature/action-call.ts +82 -0
- package/src/signature/bare-call.ts +55 -0
- package/src/signature/call-shape.ts +90 -0
- package/src/signature/call-shape.types.ts +34 -0
- package/src/signature/declared-shape.ts +52 -0
- package/src/signature/index.ts +11 -0
- package/src/signature/paren-call.ts +77 -0
- package/src/signature/render-shape.ts +92 -0
- package/src/signature/shape-at.ts +35 -0
- package/src/signature/signature-help.ts +98 -0
- package/src/symbols/document-symbols.ts +68 -0
- package/src/symbols/index.ts +1 -0
- package/src/testing/fixtures/alias-import.vn +16 -0
- package/src/testing/fixtures/shared/auth.vn +33 -0
- package/src/testing/fixtures/venn.toml +8 -0
- package/src/testing/lsp-fixture.ts +77 -0
- package/src/types/index.ts +3 -0
- package/src/types/type-service.ts +144 -0
- package/src/types/warm-types.ts +30 -0
- package/src/validation/check-validator.ts +111 -0
- package/src/validation/index.ts +1 -0
- package/src/workspace/import-resolver.ts +187 -0
- 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
|
+
}
|
package/src/env/index.ts
ADDED
|
@@ -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";
|