@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,38 @@
1
+ import type { AstNode } from "@venn-lang/core";
2
+
3
+ /**
4
+ * What a name under the cursor turns out to be.
5
+ *
6
+ * The kinds differ in one thing that decides everything downstream: how far the
7
+ * name reaches. A `fragment`, a `deco` and a `pub fn` cross files, so finding
8
+ * every use means reading the workspace. A `const`, a parameter or a loop
9
+ * variable is bound in one place in one file, and a name spelled the same
10
+ * anywhere else is a different name: following it out of the file would report
11
+ * strangers as references.
12
+ */
13
+ export type SymbolKind = "fragment" | "deco" | "fn" | "type" | "binding" | "external";
14
+
15
+ export interface FoundSymbol {
16
+ kind: SymbolKind;
17
+ name: string;
18
+ /**
19
+ * The node that binds it, when the kind is file-scoped. Two `const`s of the
20
+ * same name in one file are two symbols, and this is what tells them apart.
21
+ */
22
+ binding?: AstNode;
23
+ }
24
+
25
+ /** One place a name appears, named by the property of the node that holds it. */
26
+ export interface Occurrence {
27
+ node: AstNode;
28
+ property: string;
29
+ /** For a list-valued property: the names inside `import { a, b }`. */
30
+ index?: number;
31
+ /** Whether this is where the name is introduced, rather than used. */
32
+ declaration: boolean;
33
+ }
34
+
35
+ /** Whether a symbol's uses can be in another file at all. */
36
+ export function crossesFiles(kind: SymbolKind): boolean {
37
+ return kind !== "binding" && kind !== "type";
38
+ }
@@ -0,0 +1,15 @@
1
+ import { type Document, isDecoDecl } from "@venn-lang/core";
2
+ import type { LangiumDocument, LangiumDocuments } from "langium";
3
+
4
+ /** Whether any open document declares a `deco` by this name. */
5
+ export function declaresDeco(documents: LangiumDocuments, name: string): boolean {
6
+ for (const document of documents.all) {
7
+ const decls = rootOf(document)?.decls ?? [];
8
+ if (decls.some((decl) => isDecoDecl(decl) && decl.name === name)) return true;
9
+ }
10
+ return false;
11
+ }
12
+
13
+ function rootOf(document: LangiumDocument): Document | undefined {
14
+ return document.parseResult?.value as Document | undefined;
15
+ }
@@ -0,0 +1 @@
1
+ export { VennRenameProvider } from "./rename.js";
@@ -0,0 +1,73 @@
1
+ import type { LangiumDocument, LangiumDocuments } from "langium";
2
+ import type { RenameProvider } from "langium/lsp";
3
+ import type {
4
+ PrepareRenameParams,
5
+ Range,
6
+ RenameParams,
7
+ TextEdit,
8
+ WorkspaceEdit,
9
+ } from "vscode-languageserver";
10
+ import {
11
+ type FoundSymbol,
12
+ findOccurrences,
13
+ leafAt,
14
+ type Occurrence,
15
+ rangeOf,
16
+ symbolAt,
17
+ } from "../references/index.js";
18
+ import type { VennServices } from "../services/lsp.types.js";
19
+ import { declaresDeco } from "./declares-deco.js";
20
+
21
+ /**
22
+ * F2: rename a name wherever it means this one.
23
+ *
24
+ * Reads the same occurrences "find all references" reports, so the two cannot
25
+ * disagree. A rename that silently misses a use is worse than no rename at all.
26
+ */
27
+ export class VennRenameProvider implements RenameProvider {
28
+ private readonly documents: LangiumDocuments;
29
+
30
+ constructor(services: VennServices) {
31
+ this.documents = services.shared.workspace.LangiumDocuments;
32
+ }
33
+
34
+ rename(document: LangiumDocument, params: RenameParams): WorkspaceEdit | undefined {
35
+ const symbol = this.wanted(document, params);
36
+ if (!symbol) return undefined;
37
+ const changes: Record<string, TextEdit[]> = {};
38
+ for (const found of findOccurrences({ symbol, document, documents: this.documents })) {
39
+ const edits = rewrites(found.occurrences, params.newName);
40
+ if (edits.length > 0) changes[found.uri] = edits;
41
+ }
42
+ return Object.keys(changes).length > 0 ? { changes } : undefined;
43
+ }
44
+
45
+ prepareRename(document: LangiumDocument, params: PrepareRenameParams): Range | undefined {
46
+ return this.wanted(document, params) ? leafAt(document, params.position)?.range : undefined;
47
+ }
48
+
49
+ /**
50
+ * The symbol under the cursor, when it is one this can rewrite.
51
+ *
52
+ * A built-in decorator has no source to rewrite, and changing one use of it
53
+ * would break the program while looking like a rename.
54
+ */
55
+ private wanted(
56
+ document: LangiumDocument,
57
+ params: { position: PrepareRenameParams["position"] },
58
+ ): FoundSymbol | undefined {
59
+ const symbol = symbolAt(document, params.position);
60
+ if (!symbol) return undefined;
61
+ if (symbol.kind === "deco" && !declaresDeco(this.documents, symbol.name)) return undefined;
62
+ return symbol;
63
+ }
64
+ }
65
+
66
+ function rewrites(occurrences: readonly Occurrence[], newName: string): TextEdit[] {
67
+ const edits: TextEdit[] = [];
68
+ for (const each of occurrences) {
69
+ const range = rangeOf(each);
70
+ if (range) edits.push({ range, newText: newName });
71
+ }
72
+ return edits;
73
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ type ActionCall,
3
+ isActionCall,
4
+ isAnnotation,
5
+ isMatcherClause,
6
+ isRunStmt,
7
+ } from "@venn-lang/core";
8
+ import { GrammarUtils } from "langium";
9
+ import { SemanticTokenModifiers, SemanticTokenTypes } from "vscode-languageserver";
10
+ import type { HighlightArgs } from "./highlight.types.js";
11
+
12
+ /** Everything that reads as an invocation: actions, matchers, fragments, annotations. */
13
+ export function highlightCalls(args: HighlightArgs): void {
14
+ const { node, acceptor } = args;
15
+ if (isActionCall(node)) target(node, args);
16
+ else if (isMatcherClause(node))
17
+ acceptor({ node, property: "name", type: SemanticTokenTypes.method });
18
+ else if (isAnnotation(node))
19
+ acceptor({ node, property: "name", type: SemanticTokenTypes.decorator });
20
+ else if (isRunStmt(node)) runTarget(args);
21
+ }
22
+
23
+ function runTarget(args: HighlightArgs): void {
24
+ const { node, acceptor } = args;
25
+ if (!isRunStmt(node)) return;
26
+ // The same type the declaration carries: what `run` names is a fragment, and
27
+ // the two places the reader sees that name should agree about what it is.
28
+ acceptor({ node, property: "target", type: SemanticTokenTypes.macro });
29
+ if (node.bind) {
30
+ const type = SemanticTokenTypes.variable;
31
+ acceptor({ node, property: "bind", type, modifier: SemanticTokenModifiers.declaration });
32
+ }
33
+ }
34
+
35
+ // `http.get` is a single grammar token: colour the namespace and the action apart.
36
+ function target(node: ActionCall, args: HighlightArgs): void {
37
+ const cst = GrammarUtils.findNodeForProperty(node.$cstNode, "target");
38
+ if (!cst) return;
39
+ const dot = node.target.indexOf(".");
40
+ const { line, character } = cst.range.start;
41
+ const known = dot > 0 && args.catalog.hasNamespace(node.target.slice(0, dot));
42
+ const modifier = known ? SemanticTokenModifiers.defaultLibrary : [];
43
+ if (dot > 0) {
44
+ const options = { line, char: character, length: dot, type: SemanticTokenTypes.namespace };
45
+ args.acceptor({ ...options, modifier });
46
+ }
47
+ const from = dot > 0 ? dot + 1 : 0;
48
+ const length = node.target.length - from;
49
+ args.acceptor({
50
+ line,
51
+ char: character + from,
52
+ length,
53
+ type: SemanticTokenTypes.function,
54
+ modifier,
55
+ });
56
+ }
@@ -0,0 +1,135 @@
1
+ import {
2
+ EXPRESSION_OFFSET,
3
+ type Expr,
4
+ type InterpolationSlot,
5
+ isCall,
6
+ isMember,
7
+ isPrelude,
8
+ isRef,
9
+ isStringLit,
10
+ type Member,
11
+ parseExpression,
12
+ scanInterpolations,
13
+ } from "@venn-lang/core";
14
+ import { AstUtils, type CstNode, GrammarUtils } from "langium";
15
+ import type { SemanticTokenAcceptor } from "langium/lsp";
16
+ import { type Range, SemanticTokenTypes } from "vscode-languageserver";
17
+ import type { TextDocument } from "vscode-languageserver-textdocument";
18
+ import type { SymbolCatalog } from "../catalog/index.js";
19
+ import { pathOf } from "../document/index.js";
20
+ import type { HighlightArgs } from "./highlight.types.js";
21
+
22
+ interface Segment {
23
+ offset: number;
24
+ length: number;
25
+ type: string;
26
+ }
27
+
28
+ /**
29
+ * A string holding `${…}` is not one string token: the placeholders carry code,
30
+ * and code the editor paints as prose is code nobody can read.
31
+ *
32
+ * Returns whether it claimed the node, so the plain-literal pass leaves it
33
+ * alone: semantic tokens must not overlap.
34
+ */
35
+ export function highlightInterpolation(args: HighlightArgs): boolean {
36
+ const { node, acceptor } = args;
37
+ if (!isStringLit(node) || !node.$cstNode) return false;
38
+ const raw = node.$cstNode.text;
39
+ const slots = scanInterpolations(raw);
40
+ if (slots.length === 0) return false;
41
+ const doc = AstUtils.getDocument(node).textDocument;
42
+ emitSegments({ acceptor, doc, raw, base: node.$cstNode.offset, slots, catalog: args.catalog });
43
+ return true;
44
+ }
45
+
46
+ interface EmitArgs {
47
+ acceptor: SemanticTokenAcceptor;
48
+ doc: TextDocument;
49
+ base: number;
50
+ catalog: SymbolCatalog;
51
+ }
52
+
53
+ function emitSegments(args: EmitArgs & { raw: string; slots: readonly InterpolationSlot[] }): void {
54
+ let cursor = 0;
55
+ for (const slot of args.slots) {
56
+ const tail = slot.sourceStart + slot.source.length;
57
+ emit(args, { offset: cursor, length: slot.start - cursor, type: SemanticTokenTypes.string });
58
+ emit(args, { offset: slot.start, length: 2, type: SemanticTokenTypes.operator });
59
+ for (const part of expressionSegments(slot, args.catalog)) emit(args, part);
60
+ emit(args, { offset: tail, length: slot.end - tail, type: SemanticTokenTypes.operator });
61
+ cursor = slot.end;
62
+ }
63
+ emit(args, { offset: cursor, length: args.raw.length - cursor, type: SemanticTokenTypes.string });
64
+ }
65
+
66
+ /** One token. Empty spans are skipped: a placeholder can sit flush to a quote. */
67
+ function emit(args: EmitArgs, segment: Segment): void {
68
+ if (segment.length <= 0) return;
69
+ args.acceptor({
70
+ range: rangeAt(args.doc, args.base + segment.offset, segment.length),
71
+ type: segment.type,
72
+ });
73
+ }
74
+
75
+ function rangeAt(doc: TextDocument, offset: number, length: number): Range {
76
+ return { start: doc.positionAt(offset), end: doc.positionAt(offset + length) };
77
+ }
78
+
79
+ /**
80
+ * The tokens inside one placeholder, in document order. A placeholder that does
81
+ * not parse stays plain string; the validator is what complains about it.
82
+ */
83
+ function expressionSegments(slot: InterpolationSlot, catalog: SymbolCatalog): Segment[] {
84
+ const expr = parseExpression(slot.source);
85
+ if (!expr) {
86
+ return [
87
+ { offset: slot.sourceStart, length: slot.source.length, type: SemanticTokenTypes.string },
88
+ ];
89
+ }
90
+ const shift = slot.sourceStart - EXPRESSION_OFFSET;
91
+ return namedParts(expr, catalog)
92
+ .map((part) => ({ ...part, offset: part.offset + shift }))
93
+ .sort((left, right) => left.offset - right.offset);
94
+ }
95
+
96
+ /**
97
+ * Classify the names inside a placeholder the same way code outside one is
98
+ * classified: a namespace head reads as a namespace, a called member as a
99
+ * method, a plain read as a property. Anything less and `${fmt.json(xs.map(f))}`
100
+ * comes out as flat prose.
101
+ */
102
+ function namedParts(expr: Expr, catalog: SymbolCatalog): Segment[] {
103
+ const found: Segment[] = [];
104
+ for (const node of AstUtils.streamAst(expr)) {
105
+ if (isRef(node)) collect(found, node.$cstNode, headType(node.name, catalog));
106
+ else if (isMember(node)) {
107
+ const cst = GrammarUtils.findNodeForProperty(node.$cstNode, "member");
108
+ collect(found, cst, memberTokenType(node, catalog));
109
+ }
110
+ }
111
+ return found;
112
+ }
113
+
114
+ /** The head of `fmt.json` is a namespace; a prelude name reads as a function. */
115
+ function headType(name: string, catalog: SymbolCatalog): string {
116
+ if (catalog.hasNamespace(name)) return SemanticTokenTypes.namespace;
117
+ return isPrelude(name) ? SemanticTokenTypes.function : SemanticTokenTypes.variable;
118
+ }
119
+
120
+ function memberTokenType(node: Member, catalog: SymbolCatalog): string {
121
+ const path = pathOf(node);
122
+ const dot = path?.indexOf(".") ?? -1;
123
+ if (path && dot > 0 && catalog.hasNamespace(path.slice(0, dot))) {
124
+ return catalog.action(path.slice(0, dot), path.slice(dot + 1))
125
+ ? SemanticTokenTypes.function
126
+ : SemanticTokenTypes.property;
127
+ }
128
+ return isCall(node.$container) && node.$container.callee === node
129
+ ? SemanticTokenTypes.method
130
+ : SemanticTokenTypes.property;
131
+ }
132
+
133
+ function collect(into: Segment[], cst: CstNode | undefined, type: string): void {
134
+ if (cst) into.push({ offset: cst.offset, length: cst.length, type });
135
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ type AstNode,
3
+ isBreakStmt,
4
+ isCaptureStmt,
5
+ isConfigDecl,
6
+ isContinueStmt,
7
+ isDatasetDecl,
8
+ isDecoDecl,
9
+ isDocument,
10
+ isExpectStmt,
11
+ isFactoryDecl,
12
+ isFlowDecl,
13
+ isFnDecl,
14
+ isForEachStmt,
15
+ isFragmentDecl,
16
+ isGroupDecl,
17
+ isIfStmt,
18
+ isLetStmt,
19
+ isLifecycleDecl,
20
+ isMatrixDecl,
21
+ isParallelStmt,
22
+ isRaceStmt,
23
+ isRepeatStmt,
24
+ isReportDecl,
25
+ isReturnStmt,
26
+ isRunStmt,
27
+ isStepDecl,
28
+ isTryStmt,
29
+ isTypeDecl,
30
+ isUseDecl,
31
+ isValueImport,
32
+ isWhileStmt,
33
+ } from "@venn-lang/core";
34
+ import type { SemanticTokenAcceptor } from "langium/lsp";
35
+ import { SemanticTokenModifiers, SemanticTokenTypes } from "vscode-languageserver";
36
+
37
+ // §23: `keyword.declaration` is the LSP `keyword` type plus the `declaration`
38
+ // modifier, for the words that introduce a name. The rest are plain keywords.
39
+ const DECLARING = new Set(["module", "use", "import", "fn", "deco", "fragment", "type"]);
40
+
41
+ /** Emit a keyword token for each literal keyword this node owns. */
42
+ export function highlightKeywords(node: AstNode, acceptor: SemanticTokenAcceptor): void {
43
+ for (const word of keywordsOf(node)) {
44
+ acceptor({
45
+ node,
46
+ keyword: word,
47
+ type: SemanticTokenTypes.keyword,
48
+ modifier: DECLARING.has(word) ? SemanticTokenModifiers.declaration : [],
49
+ });
50
+ }
51
+ }
52
+
53
+ function keywordsOf(node: AstNode): string[] {
54
+ return declarations(node) ?? bindings(node) ?? control(node) ?? [];
55
+ }
56
+
57
+ function declarations(node: AstNode): string[] | undefined {
58
+ if (isDocument(node)) return node.name ? ["module"] : [];
59
+ if (isUseDecl(node)) return node.alias ? ["use", "as"] : ["use"];
60
+ if (isValueImport(node)) return ["import", "from"];
61
+ if (isFlowDecl(node)) return ["flow"];
62
+ if (isFragmentDecl(node)) return ["fragment"];
63
+ if (isFnDecl(node)) return ["fn", "return"];
64
+ if (isDecoDecl(node)) return ["deco"];
65
+ if (isTypeDecl(node)) return ["type"];
66
+ if (isFactoryDecl(node)) return ["factory"];
67
+ if (isDatasetDecl(node)) return ["dataset"];
68
+ return undefined;
69
+ }
70
+
71
+ function bindings(node: AstNode): string[] | undefined {
72
+ if (isConfigDecl(node)) return ["config"];
73
+ if (isMatrixDecl(node)) return ["matrix"];
74
+ if (isReportDecl(node)) return ["report"];
75
+ if (isLetStmt(node)) return [node.kind];
76
+ if (isCaptureStmt(node)) return node.opts ? ["capture"] : ["capture"];
77
+ if (isRunStmt(node)) return node.bind ? ["run", "as"] : ["run"];
78
+ if (isExpectStmt(node)) return expectWords(node.modifier, node.negate);
79
+ return undefined;
80
+ }
81
+
82
+ function control(node: AstNode): string[] | undefined {
83
+ if (isStepDecl(node)) return ["step"];
84
+ if (isGroupDecl(node)) return ["group"];
85
+ if (isIfStmt(node)) return node.otherwise ? ["if", "else"] : ["if"];
86
+ if (isForEachStmt(node)) return ["forEach", "in"];
87
+ if (isRepeatStmt(node)) return node.index ? ["repeat", "as"] : ["repeat"];
88
+ if (isWhileStmt(node)) return ["while"];
89
+ if (isParallelStmt(node)) return ["parallel"];
90
+ if (isRaceStmt(node)) return ["race"];
91
+ if (isTryStmt(node)) return tryWords(node.handler, node.finalizer);
92
+ if (isLifecycleDecl(node)) return [node.hook ?? "on"];
93
+ return loops(node);
94
+ }
95
+
96
+ function loops(node: AstNode): string[] | undefined {
97
+ if (isReturnStmt(node)) return ["return"];
98
+ if (isBreakStmt(node)) return ["break"];
99
+ if (isContinueStmt(node)) return ["continue"];
100
+ return undefined;
101
+ }
102
+
103
+ function expectWords(modifier: string | undefined, negate: boolean | undefined): string[] {
104
+ const words = ["expect"];
105
+ if (modifier) words.push(modifier);
106
+ if (negate) words.push("not");
107
+ return words;
108
+ }
109
+
110
+ function tryWords(handler: unknown, finalizer: unknown): string[] {
111
+ const words = ["try"];
112
+ if (handler) words.push("catch");
113
+ if (finalizer) words.push("finally");
114
+ return words;
115
+ }
@@ -0,0 +1,38 @@
1
+ import {
2
+ isBoolLit,
3
+ isInstantLit,
4
+ isMapEntry,
5
+ isMember,
6
+ isNamedType,
7
+ isNullLit,
8
+ isNumberLit,
9
+ isRef,
10
+ isStringLit,
11
+ } from "@venn-lang/core";
12
+ import { SemanticTokenTypes } from "vscode-languageserver";
13
+ import type { HighlightArgs } from "./highlight.types.js";
14
+ import { highlightInterpolation } from "./highlight-interpolation.js";
15
+ import { highlightPath } from "./highlight-paths.js";
16
+
17
+ /** Literals, references and type names inside expressions. */
18
+ export function highlightLiterals(args: HighlightArgs): void {
19
+ const { node, acceptor } = args;
20
+ if (isStringLit(node)) {
21
+ // An interpolated string is painted piece by piece; tokens cannot overlap.
22
+ if (!highlightInterpolation(args)) {
23
+ acceptor({ node, property: "value", type: SemanticTokenTypes.string });
24
+ }
25
+ } else if (isNumberLit(node))
26
+ acceptor({ node, property: "raw", type: SemanticTokenTypes.number });
27
+ else if (isInstantLit(node))
28
+ acceptor({ node, property: "value", type: SemanticTokenTypes.number });
29
+ else if (isBoolLit(node)) acceptor({ node, property: "value", type: SemanticTokenTypes.keyword });
30
+ // A stdlib path (`http.post`) is a call, not a variable; the catalog decides.
31
+ else if (isRef(node) || isMember(node)) {
32
+ if (!highlightPath(args) && isRef(node)) {
33
+ acceptor({ node, property: "name", type: SemanticTokenTypes.variable });
34
+ }
35
+ } else if (isNamedType(node)) acceptor({ node, property: "name", type: SemanticTokenTypes.type });
36
+ else if (isMapEntry(node)) acceptor({ node, property: "key", type: SemanticTokenTypes.property });
37
+ else if (isNullLit(node)) acceptor({ node, keyword: "null", type: SemanticTokenTypes.keyword });
38
+ }
@@ -0,0 +1,36 @@
1
+ import { isDocument, isUseDecl, isValueImport, type ValueImport } from "@venn-lang/core";
2
+ import { SemanticTokenModifiers, SemanticTokenTypes } from "vscode-languageserver";
3
+ import type { HighlightArgs } from "./highlight.types.js";
4
+
5
+ const DECLARATION = SemanticTokenModifiers.declaration;
6
+
7
+ /**
8
+ * The module header. These are plain string properties in the grammar, not
9
+ * expression nodes, so nothing else would ever colour them.
10
+ */
11
+ export function highlightModule(args: HighlightArgs): void {
12
+ const { node, acceptor } = args;
13
+ if (isDocument(node) && node.name) {
14
+ acceptor({ node, property: "name", type: SemanticTokenTypes.namespace });
15
+ } else if (isUseDecl(node)) {
16
+ acceptor({ node, property: "pkg", type: SemanticTokenTypes.string });
17
+ if (node.alias) {
18
+ const type = SemanticTokenTypes.namespace;
19
+ acceptor({ node, property: "alias", type, modifier: DECLARATION });
20
+ }
21
+ } else if (isValueImport(node)) {
22
+ importedNames(node, args);
23
+ }
24
+ }
25
+
26
+ function importedNames(node: ValueImport, args: HighlightArgs): void {
27
+ const { acceptor } = args;
28
+ acceptor({ node, property: "path", type: SemanticTokenTypes.string });
29
+ node.names.forEach((_name, index) => {
30
+ acceptor({ node, property: "names", index, type: SemanticTokenTypes.function });
31
+ });
32
+ const bound = node.wildcard ?? node.default;
33
+ if (!bound) return;
34
+ const property = node.wildcard ? "wildcard" : "default";
35
+ acceptor({ node, property, type: SemanticTokenTypes.namespace, modifier: DECLARATION });
36
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ isCaptureStmt,
3
+ isDatasetDecl,
4
+ isDecoDecl,
5
+ isFactoryDecl,
6
+ isFieldDecl,
7
+ isFlowDecl,
8
+ isFnDecl,
9
+ isForEachStmt,
10
+ isFragmentDecl,
11
+ isGroupDecl,
12
+ isLetStmt,
13
+ isLifecycleDecl,
14
+ isParam,
15
+ isRepeatStmt,
16
+ isStepDecl,
17
+ isTryStmt,
18
+ isTypeDecl,
19
+ } from "@venn-lang/core";
20
+ import { SemanticTokenModifiers, SemanticTokenTypes } from "vscode-languageserver";
21
+ import type { HighlightArgs } from "./highlight.types.js";
22
+
23
+ const DECLARATION = SemanticTokenModifiers.declaration;
24
+ const CALLABLE = { type: SemanticTokenTypes.function, modifier: DECLARATION } as const;
25
+ /**
26
+ * A `fragment` is invoked, not called: `run` expands it into the steps it
27
+ * declares, which the report then records. `macro` is the standard type that
28
+ * carries that meaning, and, crucially, themes draw it apart from a function.
29
+ * Painting the two alike would claim they are one kind of thing, which is the
30
+ * mistake the checker refuses.
31
+ */
32
+ const FRAGMENT = { type: SemanticTokenTypes.macro, modifier: DECLARATION } as const;
33
+ const VALUE = { type: SemanticTokenTypes.variable, modifier: DECLARATION } as const;
34
+
35
+ /** Every name a declaration introduces, plus the titles that label a flow. */
36
+ export function highlightNames(args: HighlightArgs): void {
37
+ callableNames(args);
38
+ decoratorNames(args);
39
+ valueNames(args);
40
+ memberNames(args);
41
+ boundNames(args);
42
+ titles(args);
43
+ }
44
+
45
+ // `deco memoize` and `@memoize` are one thing seen twice, so they colour alike.
46
+ function decoratorNames(args: HighlightArgs): void {
47
+ const { node, acceptor } = args;
48
+ if (!isDecoDecl(node)) return;
49
+ acceptor({ node, property: "name", type: SemanticTokenTypes.decorator, modifier: DECLARATION });
50
+ }
51
+
52
+ function callableNames(args: HighlightArgs): void {
53
+ const { node, acceptor } = args;
54
+ if (isFragmentDecl(node)) acceptor({ node, property: "name", ...FRAGMENT });
55
+ else if (isFnDecl(node)) acceptor({ node, property: "name", ...CALLABLE });
56
+ else if (isFactoryDecl(node)) acceptor({ node, property: "name", ...CALLABLE });
57
+ }
58
+
59
+ function valueNames(args: HighlightArgs): void {
60
+ const { node, acceptor } = args;
61
+ if (isCaptureStmt(node)) acceptor({ node, property: "name", ...VALUE });
62
+ else if (isDatasetDecl(node)) acceptor({ node, property: "name", ...VALUE });
63
+ else if (isLetStmt(node)) acceptor({ node, property: "name", ...readonlyFor(node.kind) });
64
+ }
65
+
66
+ function memberNames(args: HighlightArgs): void {
67
+ const { node, acceptor } = args;
68
+ const type = SemanticTokenTypes.type;
69
+ if (isParam(node)) acceptor({ node, property: "name", type: SemanticTokenTypes.parameter });
70
+ else if (isTypeDecl(node)) acceptor({ node, property: "name", type, modifier: DECLARATION });
71
+ else if (isFieldDecl(node)) acceptor({ node, property: "name", type: "property" });
72
+ }
73
+
74
+ // Names bound by a construct rather than declared: loop variables, `catch err`, `on <event>`.
75
+ function boundNames(args: HighlightArgs): void {
76
+ const { node, acceptor } = args;
77
+ if (isForEachStmt(node)) acceptor({ node, property: "item", ...VALUE });
78
+ else if (isRepeatStmt(node) && node.index) acceptor({ node, property: "index", ...VALUE });
79
+ else if (isTryStmt(node) && node.error) acceptor({ node, property: "error", ...VALUE });
80
+ else if (isLifecycleDecl(node) && node.event) {
81
+ acceptor({ node, property: "event", type: SemanticTokenTypes.enumMember });
82
+ }
83
+ }
84
+
85
+ function titles(args: HighlightArgs): void {
86
+ const { node, acceptor } = args;
87
+ const title = { property: "title", type: SemanticTokenTypes.string } as const;
88
+ if (isFlowDecl(node)) acceptor({ node, ...title });
89
+ else if (isStepDecl(node)) acceptor({ node, ...title });
90
+ else if (isGroupDecl(node)) acceptor({ node, ...title });
91
+ }
92
+
93
+ function readonlyFor(kind: string): { type: string; modifier: string[] } {
94
+ const modifier =
95
+ kind === "const" ? [DECLARATION, SemanticTokenModifiers.readonly] : [DECLARATION];
96
+ return { type: SemanticTokenTypes.variable, modifier };
97
+ }