@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,77 @@
|
|
|
1
|
+
import { type AstNode, createContext, isPrelude, resolveMember, type Type } from "@venn-lang/core";
|
|
2
|
+
import type { LangiumDocument } from "langium";
|
|
3
|
+
import type { SymbolCatalog } from "../catalog/index.js";
|
|
4
|
+
import { findBinding } from "../document/index.js";
|
|
5
|
+
import type { TypeService } from "../types/index.js";
|
|
6
|
+
import { actionHover } from "./render-action.js";
|
|
7
|
+
import { bindingHover } from "./render-decl.js";
|
|
8
|
+
import { memberHover, namespaceHover, preludeHover } from "./render-symbol.js";
|
|
9
|
+
|
|
10
|
+
export interface ResolveArgs {
|
|
11
|
+
/** The dotted path under the cursor, e.g. `people.maxBy`. */
|
|
12
|
+
path: string;
|
|
13
|
+
/** Which segment the cursor sits on; `0` is the head. */
|
|
14
|
+
segment: number;
|
|
15
|
+
/** A node inside the file, for looking a name up in scope. */
|
|
16
|
+
host: AstNode;
|
|
17
|
+
document: LangiumDocument;
|
|
18
|
+
catalog: SymbolCatalog;
|
|
19
|
+
types: TypeService;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Describe what a dotted path refers to, wherever it is written: a prelude verb,
|
|
24
|
+
* a plugin namespace or one of its actions, a built-in member of a value, or a
|
|
25
|
+
* binding the file declared.
|
|
26
|
+
*
|
|
27
|
+
* It resolves from the path's text rather than from parsed nodes, because an
|
|
28
|
+
* expression inside `"${…}"` is parsed apart from the document. That way the
|
|
29
|
+
* same name reads the same in both places.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveSymbol(args: ResolveArgs): string | undefined {
|
|
32
|
+
const segments = args.path.split(".");
|
|
33
|
+
const head = segments[0];
|
|
34
|
+
if (!head) return undefined;
|
|
35
|
+
const binding = findBinding(args.host, head);
|
|
36
|
+
if (args.segment === 0) return headHover({ head, binding, args });
|
|
37
|
+
if (!binding && args.catalog.hasNamespace(head)) return actionHover(args.path, args.catalog);
|
|
38
|
+
return memberOf({ segments, binding, args });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The first segment: something the file bound, a namespace, or a prelude name. */
|
|
42
|
+
function headHover(input: {
|
|
43
|
+
head: string;
|
|
44
|
+
binding: AstNode | undefined;
|
|
45
|
+
args: ResolveArgs;
|
|
46
|
+
}): string | undefined {
|
|
47
|
+
const { head, binding, args } = input;
|
|
48
|
+
if (binding) {
|
|
49
|
+
return bindingHover({
|
|
50
|
+
document: args.document,
|
|
51
|
+
node: args.host,
|
|
52
|
+
name: head,
|
|
53
|
+
types: args.types,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return namespaceHover(head, args.catalog) ?? (isPrelude(head) ? preludeHover(head) : undefined);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `xs.map`, `cfg.server.port`: walk to the receiver, then describe its member. */
|
|
60
|
+
function memberOf(input: {
|
|
61
|
+
segments: readonly string[];
|
|
62
|
+
binding: AstNode | undefined;
|
|
63
|
+
args: ResolveArgs;
|
|
64
|
+
}): string | undefined {
|
|
65
|
+
const { segments, binding, args } = input;
|
|
66
|
+
const member = segments[args.segment];
|
|
67
|
+
let receiver = binding && args.types.of(args.document).types.get(binding);
|
|
68
|
+
if (!receiver || !member) return undefined;
|
|
69
|
+
for (const step of segments.slice(1, args.segment)) {
|
|
70
|
+
const next = resolveMember(receiver, step, createContext());
|
|
71
|
+
if (!next) return undefined;
|
|
72
|
+
receiver = next;
|
|
73
|
+
}
|
|
74
|
+
return memberHover({ receiver, member });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type { Type };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type AstNode, isExpr, prune, showType, type Type } from "@venn-lang/core";
|
|
2
|
+
import { AstUtils, type CstNode } from "langium";
|
|
3
|
+
import type { TypeService } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
/** The inferred type of a node: `list<number>`, `fn(number) -> number`. */
|
|
6
|
+
export function typeLabel(node: AstNode | undefined, types: TypeService): string | undefined {
|
|
7
|
+
if (!node) return undefined;
|
|
8
|
+
const found = types.of(AstUtils.getDocument(node)).types.get(node);
|
|
9
|
+
return found ? spread(found) : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Past this many fields a shape stops reading as a line and starts reading as a wall. */
|
|
13
|
+
const INLINE_FIELDS = 3;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The same type, one field per line once there are enough of them to matter.
|
|
17
|
+
*
|
|
18
|
+
* A diagnostic wants a type on one line, which is what `showType` gives. A
|
|
19
|
+
* hover has room, and a decorator handle carrying ten verbs on one line is
|
|
20
|
+
* exactly as much use as no hover at all.
|
|
21
|
+
*/
|
|
22
|
+
function spread(type: Type): string {
|
|
23
|
+
const pruned = prune(type);
|
|
24
|
+
if (pruned.kind !== "record" || pruned.fields.size <= INLINE_FIELDS) return showType(pruned);
|
|
25
|
+
const fields = [...pruned.fields].map(([name, field]) => ` ${name}: ${showType(field)},`);
|
|
26
|
+
return `{\n${fields.join("\n")}\n}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The type of the expression a token belongs to, as a hover. Used when nothing
|
|
31
|
+
* else describes the token, so hovering `.len` or a literal still answers.
|
|
32
|
+
*/
|
|
33
|
+
export function inferredType(leaf: CstNode, types: TypeService): string | undefined {
|
|
34
|
+
if (!carriesMeaning(leaf.text)) return undefined;
|
|
35
|
+
const known = types.of(AstUtils.getDocument(leaf.astNode)).types;
|
|
36
|
+
const type = nearestType(leaf, known);
|
|
37
|
+
return type ? `\`\`\`venn\n${showType(type)}\n\`\`\`` : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const NAME_OR_LITERAL = /^([A-Za-z_]\w*|\d|["'])/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Only a name or a literal describes a value. A bracket or a comma belongs to
|
|
44
|
+
* the expression around it, so answering for one would make hovering `(`
|
|
45
|
+
* report the type of whatever it encloses.
|
|
46
|
+
*/
|
|
47
|
+
function carriesMeaning(text: string): boolean {
|
|
48
|
+
return NAME_OR_LITERAL.test(text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Walk from the token's node outward to the nearest expression that has a type. */
|
|
52
|
+
function nearestType(leaf: CstNode, types: ReadonlyMap<AstNode, Type>): Type | undefined {
|
|
53
|
+
let node: AstNode | undefined = leaf.astNode;
|
|
54
|
+
while (node) {
|
|
55
|
+
if (isExpr(node)) {
|
|
56
|
+
const type = types.get(node);
|
|
57
|
+
if (type) return type;
|
|
58
|
+
}
|
|
59
|
+
node = node.$container;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @venn-lang/lsp: the Venn language server. Diagnostics, semantic highlighting,
|
|
2
|
+
// hover, go-to-definition, completion and the document outline.
|
|
3
|
+
|
|
4
|
+
export type { ActionEntry, MatcherEntry, SymbolCatalog } from "./catalog/index.js";
|
|
5
|
+
export { buildCatalog } from "./catalog/index.js";
|
|
6
|
+
export type { CompletionContext } from "./completion/index.js";
|
|
7
|
+
export { contextAt, modulePaths, VennCompletionProvider } from "./completion/index.js";
|
|
8
|
+
export type { DecoInfo, DecoScope } from "./deco/index.js";
|
|
9
|
+
export { builtinDecos, decoNamed, decosInScope } from "./deco/index.js";
|
|
10
|
+
export { VennDefinitionProvider } from "./definition/index.js";
|
|
11
|
+
export type { DocBlock, DocParam } from "./docs/index.js";
|
|
12
|
+
export { parseDoc, readDoc, renderDoc } from "./docs/index.js";
|
|
13
|
+
export type { FragmentLocation } from "./document/index.js";
|
|
14
|
+
export {
|
|
15
|
+
exportedNames,
|
|
16
|
+
findBinding,
|
|
17
|
+
findFragment,
|
|
18
|
+
importedNames,
|
|
19
|
+
resolveFragment,
|
|
20
|
+
} from "./document/index.js";
|
|
21
|
+
export { VennFormatter } from "./formatting/index.js";
|
|
22
|
+
export { VennHoverProvider } from "./hover/index.js";
|
|
23
|
+
export {
|
|
24
|
+
findOccurrences,
|
|
25
|
+
occurrencesIn,
|
|
26
|
+
symbolAt,
|
|
27
|
+
VennDocumentHighlightProvider,
|
|
28
|
+
VennReferencesProvider,
|
|
29
|
+
} from "./references/index.js";
|
|
30
|
+
export { VennRenameProvider } from "./rename/index.js";
|
|
31
|
+
export { VennSemanticTokenProvider } from "./semantic/index.js";
|
|
32
|
+
export { startVennServer } from "./server/index.js";
|
|
33
|
+
export type { VennAddedServices, VennServices } from "./services/index.js";
|
|
34
|
+
export { createVennLspServices } from "./services/index.js";
|
|
35
|
+
export { VennSignatureHelpProvider } from "./signature/index.js";
|
|
36
|
+
export { VennDocumentSymbolProvider } from "./symbols/index.js";
|
|
37
|
+
export { registerVennChecks } from "./validation/index.js";
|
|
38
|
+
export { createImportResolver, type ImportResolver } from "./workspace/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { code, fence, labelled, rule, sections } from "./markdown.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** A fenced `venn` block: the signature line at the top of a hover. */
|
|
2
|
+
export function fence(source: string): string {
|
|
3
|
+
return ["```venn", source, "```"].join("\n");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** An inline code span. */
|
|
7
|
+
export function code(text: string): string {
|
|
8
|
+
return `\`${text}\``;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hovers read best with three levels of separation, strongest to weakest:
|
|
13
|
+
*
|
|
14
|
+
* - {@link rule}: a horizontal line between the signature, the body and the
|
|
15
|
+
* provenance footer. This is what VS Code's own hovers use.
|
|
16
|
+
* - {@link sections}: a blank line between blocks inside the body.
|
|
17
|
+
* - a plain newline: a label and the content it introduces stay glued together.
|
|
18
|
+
*
|
|
19
|
+
* The blank line before `---` matters: markdown would otherwise read it as a
|
|
20
|
+
* setext heading and turn the preceding paragraph into an `<h2>`.
|
|
21
|
+
*/
|
|
22
|
+
export function rule(parts: Array<string | undefined>): string {
|
|
23
|
+
return present(parts).join("\n\n---\n\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Join the blocks that exist, separated by blank lines. */
|
|
27
|
+
export function sections(parts: Array<string | undefined>): string {
|
|
28
|
+
return present(parts).join("\n\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Glue a label to the content it introduces, with no gap between them. */
|
|
32
|
+
export function labelled(label: string, content: string): string {
|
|
33
|
+
return `${label}\n${content}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function present(parts: Array<string | undefined>): string[] {
|
|
37
|
+
return parts.filter((part): part is string => Boolean(part));
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Document } from "@venn-lang/core";
|
|
2
|
+
import type { LangiumDocument } from "langium";
|
|
3
|
+
import type { DocumentHighlightProvider } from "langium/lsp";
|
|
4
|
+
import {
|
|
5
|
+
type DocumentHighlight,
|
|
6
|
+
DocumentHighlightKind,
|
|
7
|
+
type DocumentHighlightParams,
|
|
8
|
+
} from "vscode-languageserver";
|
|
9
|
+
import { rangeOf } from "./occurrence-range.js";
|
|
10
|
+
import { occurrencesIn } from "./occurrences.js";
|
|
11
|
+
import type { Occurrence } from "./symbol.types.js";
|
|
12
|
+
import { symbolAt } from "./symbol-at.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The other places the name under the cursor appears, marked in this file.
|
|
16
|
+
*
|
|
17
|
+
* Only this file, whatever the symbol's reach: highlighting is about the page
|
|
18
|
+
* being read. Where the name is introduced is drawn apart from where it is
|
|
19
|
+
* used, which is the quickest way to see that a value is written once and read
|
|
20
|
+
* four times, or written twice, which is usually the bug being looked for.
|
|
21
|
+
*/
|
|
22
|
+
export class VennDocumentHighlightProvider implements DocumentHighlightProvider {
|
|
23
|
+
getDocumentHighlight(
|
|
24
|
+
document: LangiumDocument,
|
|
25
|
+
params: DocumentHighlightParams,
|
|
26
|
+
): DocumentHighlight[] {
|
|
27
|
+
const symbol = symbolAt(document, params.position);
|
|
28
|
+
const root = document.parseResult?.value as Document | undefined;
|
|
29
|
+
if (!symbol || !root) return [];
|
|
30
|
+
return marked(occurrencesIn({ root, symbol }));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function marked(occurrences: readonly Occurrence[]): DocumentHighlight[] {
|
|
35
|
+
const found: DocumentHighlight[] = [];
|
|
36
|
+
for (const each of occurrences) {
|
|
37
|
+
const range = rangeOf(each);
|
|
38
|
+
if (range) found.push({ range, kind: kindOf(each) });
|
|
39
|
+
}
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function kindOf(occurrence: Occurrence): DocumentHighlightKind {
|
|
44
|
+
return occurrence.declaration ? DocumentHighlightKind.Write : DocumentHighlightKind.Read;
|
|
45
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Document } from "@venn-lang/core";
|
|
2
|
+
import type { LangiumDocument, LangiumDocuments } from "langium";
|
|
3
|
+
import type { Location, Range } from "vscode-languageserver";
|
|
4
|
+
import { rangesOf } from "./occurrence-range.js";
|
|
5
|
+
import { occurrencesIn } from "./occurrences.js";
|
|
6
|
+
import { crossesFiles, type FoundSymbol, type Occurrence } from "./symbol.types.js";
|
|
7
|
+
|
|
8
|
+
/** One document's share of the answer. */
|
|
9
|
+
export interface FoundIn {
|
|
10
|
+
uri: string;
|
|
11
|
+
occurrences: readonly Occurrence[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Every place a symbol appears, in the file that asked and in the workspace
|
|
16
|
+
* when the symbol reaches that far.
|
|
17
|
+
*
|
|
18
|
+
* A file-scoped binding is never searched for elsewhere. That is not an
|
|
19
|
+
* optimisation: a `const` named `user` next door is a different `user`, and
|
|
20
|
+
* offering it as a reference sends the reader somewhere unrelated.
|
|
21
|
+
*/
|
|
22
|
+
export function findOccurrences(args: {
|
|
23
|
+
symbol: FoundSymbol;
|
|
24
|
+
document: LangiumDocument;
|
|
25
|
+
documents: LangiumDocuments;
|
|
26
|
+
}): FoundIn[] {
|
|
27
|
+
const here = inDocument(args.document, args.symbol);
|
|
28
|
+
if (!crossesFiles(args.symbol.kind)) return here ? [here] : [];
|
|
29
|
+
const found: FoundIn[] = [];
|
|
30
|
+
for (const document of args.documents.all) {
|
|
31
|
+
const one = inDocument(document, args.symbol);
|
|
32
|
+
if (one) found.push(one);
|
|
33
|
+
}
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function inDocument(document: LangiumDocument, symbol: FoundSymbol): FoundIn | undefined {
|
|
38
|
+
const root = document.parseResult?.value as Document | undefined;
|
|
39
|
+
if (!root) return undefined;
|
|
40
|
+
const occurrences = occurrencesIn({ root, symbol });
|
|
41
|
+
return occurrences.length > 0 ? { uri: document.uri.toString(), occurrences } : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The same answer as locations, which is the shape the editor asks for. */
|
|
45
|
+
export function locationsOf(found: readonly FoundIn[]): Location[] {
|
|
46
|
+
return found.flatMap((one) =>
|
|
47
|
+
rangesOf(one.occurrences).map((range: Range) => ({ uri: one.uri, range })),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { VennDocumentHighlightProvider } from "./document-highlight.js";
|
|
2
|
+
export { type FoundIn, findOccurrences, locationsOf } from "./find-occurrences.js";
|
|
3
|
+
export { nameProperty } from "./name-property.js";
|
|
4
|
+
export { rangeOf, rangesOf } from "./occurrence-range.js";
|
|
5
|
+
export { occurrencesIn } from "./occurrences.js";
|
|
6
|
+
export { VennReferencesProvider } from "./references.js";
|
|
7
|
+
export {
|
|
8
|
+
crossesFiles,
|
|
9
|
+
type FoundSymbol,
|
|
10
|
+
type Occurrence,
|
|
11
|
+
type SymbolKind,
|
|
12
|
+
} from "./symbol.types.js";
|
|
13
|
+
export { leafAt, symbolAt } from "./symbol-at.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type AstNode, isForEachStmt, isRepeatStmt } from "@venn-lang/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which property of a declaration carries the name it binds.
|
|
5
|
+
*
|
|
6
|
+
* Most spell it `name`; a `forEach` calls its variable `item` and a `repeat`
|
|
7
|
+
* calls its counter `index`. Kept in one place because every reader of a
|
|
8
|
+
* declaration needs the answer, and copies of it drift.
|
|
9
|
+
*/
|
|
10
|
+
export function nameProperty(node: AstNode | object): string | undefined {
|
|
11
|
+
if (isForEachStmt(node)) return "item";
|
|
12
|
+
if (isRepeatStmt(node)) return "index";
|
|
13
|
+
return "name" in node ? "name" : undefined;
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { GrammarUtils } from "langium";
|
|
2
|
+
import type { Range } from "vscode-languageserver";
|
|
3
|
+
import type { Occurrence } from "./symbol.types.js";
|
|
4
|
+
|
|
5
|
+
/** Where an occurrence sits in the text, or nothing when the CST has no node. */
|
|
6
|
+
export function rangeOf(occurrence: Occurrence): Range | undefined {
|
|
7
|
+
const cst = GrammarUtils.findNodeForProperty(
|
|
8
|
+
occurrence.node.$cstNode,
|
|
9
|
+
occurrence.property,
|
|
10
|
+
occurrence.index,
|
|
11
|
+
);
|
|
12
|
+
return cst?.range;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The occurrences that resolved to a real range, as ranges. */
|
|
16
|
+
export function rangesOf(occurrences: readonly Occurrence[]): Range[] {
|
|
17
|
+
const found: Range[] = [];
|
|
18
|
+
for (const each of occurrences) {
|
|
19
|
+
const range = rangeOf(each);
|
|
20
|
+
if (range) found.push(range);
|
|
21
|
+
}
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AstNode,
|
|
3
|
+
type Document,
|
|
4
|
+
isAnnotation,
|
|
5
|
+
isDecoDecl,
|
|
6
|
+
isFnDecl,
|
|
7
|
+
isFragmentDecl,
|
|
8
|
+
isNamedType,
|
|
9
|
+
isRef,
|
|
10
|
+
isRunStmt,
|
|
11
|
+
isTypeDecl,
|
|
12
|
+
isValueImport,
|
|
13
|
+
walkAst,
|
|
14
|
+
} from "@venn-lang/core";
|
|
15
|
+
import { findBinding } from "../document/index.js";
|
|
16
|
+
import { nameProperty } from "./name-property.js";
|
|
17
|
+
import type { FoundSymbol, Occurrence } from "./symbol.types.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every place one symbol appears in one document.
|
|
21
|
+
*
|
|
22
|
+
* One walk, one answer, whatever the caller wants it for: "find all references"
|
|
23
|
+
* shows them, the editor highlights them, and rename rewrites them. Three
|
|
24
|
+
* separate readings of the tree would be three chances to disagree.
|
|
25
|
+
*/
|
|
26
|
+
export function occurrencesIn(args: { root: Document; symbol: FoundSymbol }): Occurrence[] {
|
|
27
|
+
const found = importNames(args.root, args.symbol.name);
|
|
28
|
+
for (const node of walkAst(args.root)) {
|
|
29
|
+
const one = occurrenceAt({ node, symbol: args.symbol });
|
|
30
|
+
if (one) found.push(one);
|
|
31
|
+
}
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function occurrenceAt(args: { node: AstNode; symbol: FoundSymbol }): Occurrence | undefined {
|
|
36
|
+
const { node, symbol } = args;
|
|
37
|
+
if (symbol.kind === "binding") return bindingUse(node, symbol);
|
|
38
|
+
if (symbol.kind === "type") return typeUse(node, symbol.name);
|
|
39
|
+
if (symbol.kind === "deco") return decoUse(node, symbol.name);
|
|
40
|
+
if (symbol.kind === "fragment") return fragmentUse(node, symbol.name);
|
|
41
|
+
if (symbol.kind === "fn") return fnUse(node, symbol.name);
|
|
42
|
+
return externalUse(node, symbol.name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A name this file imported: found wherever a name of any importable kind can
|
|
47
|
+
* appear.
|
|
48
|
+
*
|
|
49
|
+
* Which of the three it is (fragment, function or decorator) is written in the
|
|
50
|
+
* file it came from, and this is asked of every file in the workspace, one of
|
|
51
|
+
* which is that one. Matching all three shapes reaches it without reading
|
|
52
|
+
* ahead, and no shape can match a name that is not this one.
|
|
53
|
+
*/
|
|
54
|
+
function externalUse(node: AstNode, name: string): Occurrence | undefined {
|
|
55
|
+
return fnUse(node, name) ?? fragmentUse(node, name) ?? decoUse(node, name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `import { a, b }`: one node holding several names, so each is found by index. */
|
|
59
|
+
function importNames(root: Document, name: string): Occurrence[] {
|
|
60
|
+
const found: Occurrence[] = [];
|
|
61
|
+
for (const decl of root.imports) {
|
|
62
|
+
if (!isValueImport(decl)) continue;
|
|
63
|
+
decl.names.forEach((each, index) => {
|
|
64
|
+
if (each === name) found.push({ node: decl, property: "names", index, declaration: false });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return found;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fragmentUse(node: AstNode, name: string): Occurrence | undefined {
|
|
71
|
+
if (isFragmentDecl(node) && node.name === name) return declaration(node);
|
|
72
|
+
if (isRunStmt(node) && node.target === name) {
|
|
73
|
+
return { node, property: "target", declaration: false };
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function decoUse(node: AstNode, name: string): Occurrence | undefined {
|
|
79
|
+
if (isDecoDecl(node) && node.name === name) return declaration(node);
|
|
80
|
+
if (isAnnotation(node) && node.name === name) {
|
|
81
|
+
return { node, property: "name", declaration: false };
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A function: where it is declared, and every name that reads it.
|
|
88
|
+
*
|
|
89
|
+
* Matched by name rather than by resolving each reference back to the
|
|
90
|
+
* declaration, because a `pub fn` is read in files that never declared it: the
|
|
91
|
+
* file next door holds the same name and means the same thing.
|
|
92
|
+
*/
|
|
93
|
+
function fnUse(node: AstNode, name: string): Occurrence | undefined {
|
|
94
|
+
if (isFnDecl(node) && node.name === name) return declaration(node);
|
|
95
|
+
if (isRef(node) && node.name === name) return { node, property: "name", declaration: false };
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function typeUse(node: AstNode, name: string): Occurrence | undefined {
|
|
100
|
+
if (isTypeDecl(node) && node.name === name) return declaration(node);
|
|
101
|
+
if (isNamedType(node) && node.name === name) {
|
|
102
|
+
return { node, property: "name", declaration: false };
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A binding, and only the reads that resolve back to this one.
|
|
109
|
+
*
|
|
110
|
+
* A name shadowed in an inner block is a different binding wearing the same
|
|
111
|
+
* spelling, and reporting it would send the reader to a line that has nothing
|
|
112
|
+
* to do with what they asked about.
|
|
113
|
+
*/
|
|
114
|
+
function bindingUse(node: AstNode, symbol: FoundSymbol): Occurrence | undefined {
|
|
115
|
+
if (node === symbol.binding) return declaration(node);
|
|
116
|
+
if (!isRef(node) || node.name !== symbol.name) return undefined;
|
|
117
|
+
if (findBinding(node, symbol.name) !== symbol.binding) return undefined;
|
|
118
|
+
return { node, property: "name", declaration: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function declaration(node: AstNode): Occurrence | undefined {
|
|
122
|
+
const property = nameProperty(node);
|
|
123
|
+
return property ? { node, property, declaration: true } : undefined;
|
|
124
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { LangiumDocument, LangiumDocuments } from "langium";
|
|
2
|
+
import type { ReferencesProvider } from "langium/lsp";
|
|
3
|
+
import type { Location, ReferenceParams } from "vscode-languageserver";
|
|
4
|
+
import type { VennServices } from "../services/lsp.types.js";
|
|
5
|
+
import { findOccurrences, locationsOf } from "./find-occurrences.js";
|
|
6
|
+
import { symbolAt } from "./symbol-at.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shift+F12: every place a name is used.
|
|
10
|
+
*
|
|
11
|
+
* Langium's own provider resolves cross-references the grammar declares, and
|
|
12
|
+
* this grammar declares none: a `run` target is a string, a `@name` is a string,
|
|
13
|
+
* and which `const` a `Ref` means is worked out by walking scopes. So the answer
|
|
14
|
+
* has to be worked out the same way the checker and the evaluator work it out,
|
|
15
|
+
* which is what this shares with them.
|
|
16
|
+
*/
|
|
17
|
+
export class VennReferencesProvider implements ReferencesProvider {
|
|
18
|
+
private readonly documents: LangiumDocuments;
|
|
19
|
+
|
|
20
|
+
constructor(services: VennServices) {
|
|
21
|
+
this.documents = services.shared.workspace.LangiumDocuments;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
findReferences(document: LangiumDocument, params: ReferenceParams): Location[] {
|
|
25
|
+
const symbol = symbolAt(document, params.position);
|
|
26
|
+
if (!symbol) return [];
|
|
27
|
+
const found = findOccurrences({ symbol, document, documents: this.documents });
|
|
28
|
+
const wanted = params.context?.includeDeclaration === false ? uses(found) : found;
|
|
29
|
+
return locationsOf(wanted);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The editor may ask for the uses without the line that introduces the name. */
|
|
34
|
+
function uses(found: ReturnType<typeof findOccurrences>): ReturnType<typeof findOccurrences> {
|
|
35
|
+
return found
|
|
36
|
+
.map((one) => ({ ...one, occurrences: one.occurrences.filter((each) => !each.declaration) }))
|
|
37
|
+
.filter((one) => one.occurrences.length > 0);
|
|
38
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AstNode,
|
|
3
|
+
isAnnotation,
|
|
4
|
+
isCaptureStmt,
|
|
5
|
+
isDatasetDecl,
|
|
6
|
+
isDecoDecl,
|
|
7
|
+
isDocument,
|
|
8
|
+
isFnDecl,
|
|
9
|
+
isForEachStmt,
|
|
10
|
+
isFragmentDecl,
|
|
11
|
+
isLetStmt,
|
|
12
|
+
isNamedType,
|
|
13
|
+
isParam,
|
|
14
|
+
isRef,
|
|
15
|
+
isRepeatStmt,
|
|
16
|
+
isRunStmt,
|
|
17
|
+
isTypeDecl,
|
|
18
|
+
isValueImport,
|
|
19
|
+
} from "@venn-lang/core";
|
|
20
|
+
import { AstUtils, type CstNode, CstUtils, type LangiumDocument } from "langium";
|
|
21
|
+
import type { Position } from "vscode-languageserver";
|
|
22
|
+
import { findBinding } from "../document/index.js";
|
|
23
|
+
import { nameProperty } from "./name-property.js";
|
|
24
|
+
import type { FoundSymbol } from "./symbol.types.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The symbol a position names, or nothing when it names none.
|
|
28
|
+
*
|
|
29
|
+
* Read from the token under the cursor rather than from the node containing it:
|
|
30
|
+
* one node often carries two names (`import { a, b }` is a single node, and so
|
|
31
|
+
* is `run login("x")`), and the answer depends on which word was clicked.
|
|
32
|
+
*/
|
|
33
|
+
export function symbolAt(document: LangiumDocument, position: Position): FoundSymbol | undefined {
|
|
34
|
+
const leaf = leafAt(document, position);
|
|
35
|
+
if (!leaf) return undefined;
|
|
36
|
+
return declared(leaf) ?? used(leaf);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function leafAt(document: LangiumDocument, position: Position): CstNode | undefined {
|
|
40
|
+
const root = document.parseResult?.value?.$cstNode;
|
|
41
|
+
const offset = document.textDocument.offsetAt(position);
|
|
42
|
+
return root ? CstUtils.findLeafNodeAtOffset(root, offset) : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The cursor is on the word that introduces the name. */
|
|
46
|
+
function declared(leaf: CstNode): FoundSymbol | undefined {
|
|
47
|
+
const node = leaf.astNode;
|
|
48
|
+
const name = leaf.text;
|
|
49
|
+
if (isFragmentDecl(node) && node.name === name) return { kind: "fragment", name };
|
|
50
|
+
if (isDecoDecl(node) && node.name === name) return { kind: "deco", name };
|
|
51
|
+
if (isFnDecl(node) && node.name === name) return { kind: "fn", name };
|
|
52
|
+
if (isTypeDecl(node) && node.name === name) return { kind: "type", name, binding: node };
|
|
53
|
+
return declaredBinding(leaf);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The cursor on the word a `const`, a parameter or a loop variable introduces.
|
|
58
|
+
*
|
|
59
|
+
* Told apart from a use by the node it sits on, never by the text. A `Ref` also
|
|
60
|
+
* carries a `name` equal to the word under the cursor, and treating one as a
|
|
61
|
+
* declaration would make the binding be itself, so no read of it would resolve.
|
|
62
|
+
*/
|
|
63
|
+
function declaredBinding(leaf: CstNode): FoundSymbol | undefined {
|
|
64
|
+
const node = leaf.astNode;
|
|
65
|
+
if (!binds(node)) return undefined;
|
|
66
|
+
const property = nameProperty(node);
|
|
67
|
+
const written = property && (node as unknown as Record<string, unknown>)[property];
|
|
68
|
+
return written === leaf.text ? { kind: "binding", name: leaf.text, binding: node } : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function binds(node: AstNode): boolean {
|
|
72
|
+
return (
|
|
73
|
+
isLetStmt(node) ||
|
|
74
|
+
isCaptureStmt(node) ||
|
|
75
|
+
isDatasetDecl(node) ||
|
|
76
|
+
isParam(node) ||
|
|
77
|
+
isForEachStmt(node) ||
|
|
78
|
+
isRepeatStmt(node)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The cursor is on a use of the name, or on one inside an `import { … }`. */
|
|
83
|
+
function used(leaf: CstNode): FoundSymbol | undefined {
|
|
84
|
+
const node = leaf.astNode;
|
|
85
|
+
const name = leaf.text;
|
|
86
|
+
if (isRunStmt(node) && node.target === name) return { kind: "fragment", name };
|
|
87
|
+
if (isAnnotation(node) && node.name === name) return { kind: "deco", name };
|
|
88
|
+
if (isNamedType(node) && node.name === name) return { kind: "type", name };
|
|
89
|
+
if (isValueImport(node) && node.names.includes(name)) return { kind: "external", name };
|
|
90
|
+
return isRef(node) && node.name === name ? boundSymbol(node, name) : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A plain name: which binding it resolves to decides how far it reaches.
|
|
95
|
+
*
|
|
96
|
+
* A `fn` this file declared is importable, so its uses may be anywhere; a
|
|
97
|
+
* `const` or a parameter is bound here and a name spelled the same next door is
|
|
98
|
+
* a different name entirely.
|
|
99
|
+
*/
|
|
100
|
+
function boundSymbol(node: AstNode, name: string): FoundSymbol {
|
|
101
|
+
const binding = findBinding(node, name);
|
|
102
|
+
if (binding && isFnDecl(binding)) return { kind: "fn", name };
|
|
103
|
+
if (binding && isFragmentDecl(binding)) return { kind: "fragment", name };
|
|
104
|
+
if (binding && isDecoDecl(binding)) return { kind: "deco", name };
|
|
105
|
+
// Nothing here binds it and the file imported it, so it belongs to another
|
|
106
|
+
// file. Calling it a local binding would keep the search inside this one.
|
|
107
|
+
if (!binding && isImported(node, name)) return { kind: "external", name };
|
|
108
|
+
return { kind: "binding", name, binding };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isImported(from: AstNode, name: string): boolean {
|
|
112
|
+
const root = AstUtils.getContainerOfType(from, isDocument);
|
|
113
|
+
return (root?.imports ?? []).some((decl) => isValueImport(decl) && decl.names.includes(name));
|
|
114
|
+
}
|