@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,232 @@
|
|
|
1
|
+
import { type Document, isFragmentDecl, showType, specToType } from "@venn-lang/core";
|
|
2
|
+
import { type ActionDefinition, paramSpecs } from "@venn-lang/sdk";
|
|
3
|
+
import { showSpec } from "@venn-lang/types";
|
|
4
|
+
import { type CompletionItem, CompletionItemKind, type Range } from "vscode-languageserver";
|
|
5
|
+
import type { SymbolCatalog } from "../catalog/index.js";
|
|
6
|
+
import { type DecoInfo, decoratesLabel } from "../deco/index.js";
|
|
7
|
+
import type { ExportedName } from "../document/index.js";
|
|
8
|
+
import type { EnvVar } from "../env/index.js";
|
|
9
|
+
import { ICON, iconForOrigin } from "./icons.js";
|
|
10
|
+
|
|
11
|
+
const KEYWORDS = [
|
|
12
|
+
"step",
|
|
13
|
+
"group",
|
|
14
|
+
"if",
|
|
15
|
+
"else",
|
|
16
|
+
"forEach",
|
|
17
|
+
"repeat",
|
|
18
|
+
"while",
|
|
19
|
+
"parallel",
|
|
20
|
+
"race",
|
|
21
|
+
"try",
|
|
22
|
+
"catch",
|
|
23
|
+
"finally",
|
|
24
|
+
"expect",
|
|
25
|
+
"run",
|
|
26
|
+
"let",
|
|
27
|
+
"const",
|
|
28
|
+
"return",
|
|
29
|
+
"break",
|
|
30
|
+
"continue",
|
|
31
|
+
"defer",
|
|
32
|
+
"setup",
|
|
33
|
+
"teardown",
|
|
34
|
+
"beforeEach",
|
|
35
|
+
"afterEach",
|
|
36
|
+
];
|
|
37
|
+
// Callable without a `use`: verbs that act, plus the values with no receiver.
|
|
38
|
+
const PRELUDE = [
|
|
39
|
+
"print",
|
|
40
|
+
"log",
|
|
41
|
+
"wait",
|
|
42
|
+
"skip",
|
|
43
|
+
"fail",
|
|
44
|
+
"exit",
|
|
45
|
+
"range",
|
|
46
|
+
"str",
|
|
47
|
+
"typeOf",
|
|
48
|
+
"pretty",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Every item carries an explicit `textEdit` over `range`. VS Code's default word
|
|
53
|
+
* rules stop at `@` and `/`, so without it accepting `@venn-lang/http` inside
|
|
54
|
+
* `"@venn-lang/"` would paste the prefix twice.
|
|
55
|
+
*/
|
|
56
|
+
export function item(args: {
|
|
57
|
+
label: string;
|
|
58
|
+
kind: CompletionItemKind;
|
|
59
|
+
range: Range;
|
|
60
|
+
detail?: string;
|
|
61
|
+
documentation?: string;
|
|
62
|
+
/** What to write, when it differs from the label: `body` inserts `body: `. */
|
|
63
|
+
insert?: string;
|
|
64
|
+
}): CompletionItem {
|
|
65
|
+
return {
|
|
66
|
+
label: args.label,
|
|
67
|
+
kind: args.kind,
|
|
68
|
+
detail: args.detail,
|
|
69
|
+
documentation: args.documentation,
|
|
70
|
+
filterText: args.label,
|
|
71
|
+
textEdit: { range: args.range, newText: args.insert ?? args.label },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The keys of an action's options map, read from its params schema. That is the
|
|
77
|
+
* schema the runtime validates against, so the editor cannot drift from it.
|
|
78
|
+
*/
|
|
79
|
+
export function optionItems(args: {
|
|
80
|
+
target: string;
|
|
81
|
+
catalog: SymbolCatalog;
|
|
82
|
+
range: Range;
|
|
83
|
+
}): CompletionItem[] {
|
|
84
|
+
const dot = args.target.indexOf(".");
|
|
85
|
+
if (dot < 0) return [];
|
|
86
|
+
const entry = args.catalog.action(args.target.slice(0, dot), args.target.slice(dot + 1));
|
|
87
|
+
return paramSpecs(entry?.action.params).map((spec) =>
|
|
88
|
+
item({
|
|
89
|
+
label: spec.name,
|
|
90
|
+
kind: ICON.key,
|
|
91
|
+
range: args.range,
|
|
92
|
+
detail: spec.required ? `${spec.type} (required)` : spec.type,
|
|
93
|
+
documentation: spec.doc,
|
|
94
|
+
insert: `${spec.name}: `,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `env.`: the variables `venn.toml` declares, secrets shown without values. */
|
|
100
|
+
export function envItems(vars: readonly EnvVar[], range: Range): CompletionItem[] {
|
|
101
|
+
return vars.map((variable) =>
|
|
102
|
+
item({
|
|
103
|
+
label: variable.name,
|
|
104
|
+
kind: ICON.env,
|
|
105
|
+
range,
|
|
106
|
+
detail: variable.secret ? "secret" : variable.values[0]?.value,
|
|
107
|
+
documentation:
|
|
108
|
+
variable.doc ??
|
|
109
|
+
`Declared in venn.toml for ${variable.values.map((v) => v.environment).join(", ")}.`,
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function packageItems(catalog: SymbolCatalog, range: Range): CompletionItem[] {
|
|
115
|
+
return catalog.packages().map((label) => {
|
|
116
|
+
const namespace = catalog.namespaceOfPackage(label);
|
|
117
|
+
return item({ label, kind: ICON.namespace, range, detail: namespace });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The types a namespace publishes: `http.Request`, `http.Server`.
|
|
123
|
+
*
|
|
124
|
+
* They stand beside the verbs because that is where they are written. A
|
|
125
|
+
* namespace answering only with verbs leaves the author guessing what those
|
|
126
|
+
* verbs deal in.
|
|
127
|
+
*/
|
|
128
|
+
export function typeItems(
|
|
129
|
+
namespace: string,
|
|
130
|
+
catalog: SymbolCatalog,
|
|
131
|
+
range: Range,
|
|
132
|
+
): CompletionItem[] {
|
|
133
|
+
return catalog.typesIn(namespace).map((entry) =>
|
|
134
|
+
item({
|
|
135
|
+
label: entry.name,
|
|
136
|
+
kind: ICON.type,
|
|
137
|
+
range,
|
|
138
|
+
detail: showType(specToType(entry.spec, () => undefined)),
|
|
139
|
+
documentation: `Type published by ${entry.package}.`,
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function actionItems(
|
|
145
|
+
namespace: string,
|
|
146
|
+
catalog: SymbolCatalog,
|
|
147
|
+
range: Range,
|
|
148
|
+
): CompletionItem[] {
|
|
149
|
+
return catalog.actionsIn(namespace).map((entry) =>
|
|
150
|
+
item({
|
|
151
|
+
label: entry.name,
|
|
152
|
+
kind: ICON.verb,
|
|
153
|
+
range,
|
|
154
|
+
detail: returnOf(entry.action) ?? entry.package,
|
|
155
|
+
documentation: entry.action.doc,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** What the verb answers with, taken from its declared type, never from prose. */
|
|
161
|
+
function returnOf(action: ActionDefinition): string | undefined {
|
|
162
|
+
const result = action.signature?.result;
|
|
163
|
+
if (!result || (result.kind === "prim" && result.name === "void")) return undefined;
|
|
164
|
+
return `-> ${showSpec(result)}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function matcherItems(catalog: SymbolCatalog, range: Range): CompletionItem[] {
|
|
168
|
+
return catalog
|
|
169
|
+
.matchers()
|
|
170
|
+
.map((entry) => item({ label: entry.name, kind: ICON.matcher, range, detail: entry.package }));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* After `@`: every decorator in scope, each saying what it decorates.
|
|
175
|
+
*
|
|
176
|
+
* What it decorates is the detail, not an afterthought: applying one to the
|
|
177
|
+
* wrong kind is the mistake this list exists to prevent.
|
|
178
|
+
*/
|
|
179
|
+
export function annotationItems(decos: readonly DecoInfo[], range: Range): CompletionItem[] {
|
|
180
|
+
return decos.map((deco) =>
|
|
181
|
+
item({
|
|
182
|
+
label: deco.name,
|
|
183
|
+
kind: ICON.decorator,
|
|
184
|
+
range,
|
|
185
|
+
detail: decoratesLabel(deco.decorates),
|
|
186
|
+
documentation: deco.doc,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function fragmentItems(args: {
|
|
192
|
+
document: Document | undefined;
|
|
193
|
+
/** The imported names that really are fragments, from the files they came from. */
|
|
194
|
+
imported: ReadonlySet<string>;
|
|
195
|
+
range: Range;
|
|
196
|
+
}): CompletionItem[] {
|
|
197
|
+
if (!args.document) return [];
|
|
198
|
+
const local = args.document.decls.filter(isFragmentDecl).map((decl) => decl.name);
|
|
199
|
+
return simple([...local, ...args.imported], ICON.fragment, args.range);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function pathItems(paths: readonly string[], range: Range): CompletionItem[] {
|
|
203
|
+
return simple(paths, CompletionItemKind.File, range);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function exportItems(names: readonly ExportedName[], range: Range): CompletionItem[] {
|
|
207
|
+
return names.map((each) =>
|
|
208
|
+
item({
|
|
209
|
+
label: each.name,
|
|
210
|
+
kind: iconForOrigin(each.origin),
|
|
211
|
+
range,
|
|
212
|
+
detail: `pub ${each.origin}`,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** At the start of a statement: namespaces, prelude verbs and structural keywords. */
|
|
218
|
+
export function openingItems(catalog: SymbolCatalog, range: Range): CompletionItem[] {
|
|
219
|
+
return [
|
|
220
|
+
...simple(catalog.namespaces(), CompletionItemKind.Module, range),
|
|
221
|
+
...simple(PRELUDE, CompletionItemKind.Function, range),
|
|
222
|
+
...simple(KEYWORDS, CompletionItemKind.Keyword, range),
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function simple(
|
|
227
|
+
labels: readonly string[],
|
|
228
|
+
kind: CompletionItemKind,
|
|
229
|
+
range: Range,
|
|
230
|
+
): CompletionItem[] {
|
|
231
|
+
return labels.map((label) => item({ label, kind, range }));
|
|
232
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AstNode,
|
|
3
|
+
createContext,
|
|
4
|
+
MEMBER_DOCS,
|
|
5
|
+
memberKind,
|
|
6
|
+
memberType,
|
|
7
|
+
type OpaqueType,
|
|
8
|
+
prune,
|
|
9
|
+
resolveMember,
|
|
10
|
+
showType,
|
|
11
|
+
type Type,
|
|
12
|
+
} from "@venn-lang/core";
|
|
13
|
+
import type { LangiumDocument } from "langium";
|
|
14
|
+
import type { CompletionItem, CompletionItemKind, Range } from "vscode-languageserver";
|
|
15
|
+
import { findBinding } from "../document/index.js";
|
|
16
|
+
import type { TypeService } from "../types/index.js";
|
|
17
|
+
import { ICON } from "./icons.js";
|
|
18
|
+
import { item } from "./items.js";
|
|
19
|
+
import { receiverTypeAt } from "./read-from.js";
|
|
20
|
+
|
|
21
|
+
export interface MemberArgs {
|
|
22
|
+
/** The dotted path before the cursor's dot: `p`, `cfg.server`. */
|
|
23
|
+
receiver: string;
|
|
24
|
+
/** A node inside the file, for looking the head up in scope. */
|
|
25
|
+
host: AstNode;
|
|
26
|
+
document: LangiumDocument;
|
|
27
|
+
types: TypeService;
|
|
28
|
+
range: Range;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What a value offers after a dot: the built-ins of its type, plus the fields it
|
|
33
|
+
* carries when it is a map. Driven by the same inference the checker runs, so
|
|
34
|
+
* `p` inside `people.filter(fn (p) => …)` completes as a person, not as nothing.
|
|
35
|
+
*/
|
|
36
|
+
export function memberItems(args: MemberArgs): CompletionItem[] {
|
|
37
|
+
const type = receiverType(args);
|
|
38
|
+
if (!type) return [];
|
|
39
|
+
return [...fieldItems(type, args.range), ...builtinItems(type, args.range)];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Walk the path from the binding the file declared to the type it holds here. */
|
|
43
|
+
function receiverType(args: MemberArgs): Type | undefined {
|
|
44
|
+
const segments = args.receiver.split(".");
|
|
45
|
+
const head = segments[0];
|
|
46
|
+
const binding = head ? findBinding(args.host, head) : undefined;
|
|
47
|
+
let type = binding && args.types.of(args.document).types.get(binding);
|
|
48
|
+
for (const name of segments.slice(1)) {
|
|
49
|
+
if (!type) return undefined;
|
|
50
|
+
type = resolveMember(type, name, createContext());
|
|
51
|
+
}
|
|
52
|
+
return type && prune(type);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* How the editor orders what comes after a dot.
|
|
57
|
+
*
|
|
58
|
+
* What the value is comes before what every value of its kind can do: someone
|
|
59
|
+
* who typed `price.` wants `id` and `price`, not the dozens of built-ins that
|
|
60
|
+
* are the same on every map in the language.
|
|
61
|
+
*/
|
|
62
|
+
const OWN = "0";
|
|
63
|
+
const BUILT_IN = "1";
|
|
64
|
+
|
|
65
|
+
function fieldItems(type: Type, range: Range): CompletionItem[] {
|
|
66
|
+
if (type.kind === "opaque") return publishedItems(type, range);
|
|
67
|
+
if (type.kind !== "record") return [];
|
|
68
|
+
return [...type.fields].map(([name, field]) => ({
|
|
69
|
+
...item({
|
|
70
|
+
label: name,
|
|
71
|
+
kind: kindOf(field, true),
|
|
72
|
+
range,
|
|
73
|
+
detail: showType(field),
|
|
74
|
+
documentation: "Field of this map.",
|
|
75
|
+
}),
|
|
76
|
+
sortText: `${OWN}${name}`,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Which of three things this member is.
|
|
82
|
+
*
|
|
83
|
+
* The icon answers the question the reader is about to act on: do I write
|
|
84
|
+
* brackets? A method needs `(…)`; everything else is written bare. Of the bare
|
|
85
|
+
* ones, a field is data the value carries and a property is computed on read.
|
|
86
|
+
*/
|
|
87
|
+
function kindOf(member: Type, stored: boolean): CompletionItemKind {
|
|
88
|
+
if (member.kind === "fn") return ICON.method;
|
|
89
|
+
return stored ? ICON.key : ICON.computed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What a handle publishes, and only that. A server is not a map: it answers to
|
|
94
|
+
* `close`, never to `merge`, so offering `merge` would invite code that cannot
|
|
95
|
+
* work.
|
|
96
|
+
*/
|
|
97
|
+
function publishedItems(type: OpaqueType, range: Range): CompletionItem[] {
|
|
98
|
+
return [...(type.members ?? [])].map(([name, member]) => ({
|
|
99
|
+
...item({
|
|
100
|
+
label: name,
|
|
101
|
+
kind: kindOf(member, false),
|
|
102
|
+
range,
|
|
103
|
+
detail: showType(member),
|
|
104
|
+
documentation: `Published by \`${type.name}\`.`,
|
|
105
|
+
}),
|
|
106
|
+
sortText: `${OWN}${name}`,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function builtinItems(type: Type, range: Range): CompletionItem[] {
|
|
111
|
+
const kind = memberKind(type);
|
|
112
|
+
const docs = kind ? MEMBER_DOCS[kind] : undefined;
|
|
113
|
+
if (!kind || !docs) return [];
|
|
114
|
+
return Object.entries(docs).map(([name, doc]) => {
|
|
115
|
+
const member = memberType(type, name, createContext());
|
|
116
|
+
return {
|
|
117
|
+
...item({
|
|
118
|
+
label: name,
|
|
119
|
+
kind: member ? kindOf(member, false) : ICON.method,
|
|
120
|
+
range,
|
|
121
|
+
detail: member ? showType(member) : undefined,
|
|
122
|
+
documentation: doc.example ? `${doc.doc}\n\n${doc.example}` : doc.doc,
|
|
123
|
+
}),
|
|
124
|
+
sortText: `${BUILT_IN}${name}`,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The members of whatever the node under the cursor evaluates to.
|
|
131
|
+
*
|
|
132
|
+
* For a receiver no path can name (`(1234.567).`, `f(x).`, `xs[0].`) there is
|
|
133
|
+
* nothing to look up by text, but the checker already typed the expression.
|
|
134
|
+
*/
|
|
135
|
+
export function membersOfNode(args: {
|
|
136
|
+
host: AstNode;
|
|
137
|
+
/** Where the cursor is: what tells a finished member from one being typed. */
|
|
138
|
+
at: number;
|
|
139
|
+
document: LangiumDocument;
|
|
140
|
+
types: TypeService;
|
|
141
|
+
range: Range;
|
|
142
|
+
}): CompletionItem[] {
|
|
143
|
+
const types = args.types.of(args.document).types;
|
|
144
|
+
const type = receiverTypeAt({ host: args.host, at: args.at, document: args.document, types });
|
|
145
|
+
if (!type) return [];
|
|
146
|
+
return [...fieldItems(type, args.range), ...builtinItems(type, args.range)];
|
|
147
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type Dirent, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { type URI, UriUtils } from "langium";
|
|
4
|
+
import type { ImportResolver } from "../workspace/index.js";
|
|
5
|
+
|
|
6
|
+
const EXTENSION = ".vn";
|
|
7
|
+
const MAX_DEPTH = 3;
|
|
8
|
+
const SKIP = new Set(["node_modules", "dist", ".git"]);
|
|
9
|
+
|
|
10
|
+
export interface PathArgs {
|
|
11
|
+
partial: string;
|
|
12
|
+
base: URI;
|
|
13
|
+
imports: ImportResolver;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What may follow `from "`: a `#alias/…` path from `venn.toml`, or a relative
|
|
18
|
+
* path to a sibling `.vn` file. The `#` only marks an alias; plain relative
|
|
19
|
+
* paths need no prefix.
|
|
20
|
+
*
|
|
21
|
+
* @returns Path strings ready to insert, without the surrounding quotes.
|
|
22
|
+
*/
|
|
23
|
+
export function modulePaths(args: PathArgs): string[] {
|
|
24
|
+
return args.partial.startsWith("#") ? aliasPaths(args) : relativePaths(args.base);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function aliasPaths(args: PathArgs): string[] {
|
|
28
|
+
const aliases = Object.entries(args.imports.aliases(args.base));
|
|
29
|
+
const matched = aliases.find(([key]) => args.partial.startsWith(`${key}/`));
|
|
30
|
+
if (!matched) return aliases.map(([key]) => `${key}/`);
|
|
31
|
+
const [key, folder] = matched;
|
|
32
|
+
return sourceFiles(folder).map((file) => `${key}/${file}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function relativePaths(base: URI): string[] {
|
|
36
|
+
const self = UriUtils.basename(base);
|
|
37
|
+
return sourceFiles(UriUtils.dirname(base))
|
|
38
|
+
.filter((file) => file !== self)
|
|
39
|
+
.map((file) => `./${file}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sourceFiles(folder: URI): string[] {
|
|
43
|
+
return walk(folder.fsPath, "", 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function walk(root: string, relative: string, depth: number): string[] {
|
|
47
|
+
if (depth > MAX_DEPTH) return [];
|
|
48
|
+
const found: string[] = [];
|
|
49
|
+
for (const entry of read(join(root, relative))) {
|
|
50
|
+
const child = relative ? `${relative}/${entry.name}` : entry.name;
|
|
51
|
+
if (entry.isDirectory() && !SKIP.has(entry.name)) found.push(...walk(root, child, depth + 1));
|
|
52
|
+
else if (entry.isFile() && entry.name.endsWith(EXTENSION)) found.push(child);
|
|
53
|
+
}
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function read(directory: string): Dirent[] {
|
|
58
|
+
try {
|
|
59
|
+
return readdirSync(directory, { withFileTypes: true });
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ParamSpec } from "@venn-lang/sdk";
|
|
2
|
+
import type { Range } from "vscode-languageserver";
|
|
3
|
+
import { type CompletionItem, InsertTextFormat } from "vscode-languageserver";
|
|
4
|
+
import { ICON } from "./icons.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where a verb's options are due: offer the map itself, then its keys.
|
|
8
|
+
*
|
|
9
|
+
* At `http.get("u", ▮)` the braces have to exist before anything can go inside
|
|
10
|
+
* them, so the map comes first and the keys after. Accepting either writes
|
|
11
|
+
* `{ headers: … }` in a single step.
|
|
12
|
+
*/
|
|
13
|
+
export function optionsMapItems(options: readonly ParamSpec[], range: Range): CompletionItem[] {
|
|
14
|
+
return [wholeMap(options, range), ...options.map((spec) => keyItem(spec, range))];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** `{ … }`: the options map, with the required keys already in place. */
|
|
18
|
+
function wholeMap(options: readonly ParamSpec[], range: Range): CompletionItem {
|
|
19
|
+
const required = options.filter((spec) => spec.required);
|
|
20
|
+
const inner = (required.length > 0 ? required : options.slice(0, 1))
|
|
21
|
+
.map((spec, index) => `${spec.name}: \${${index + 1}:${spec.type}}`)
|
|
22
|
+
.join(", ");
|
|
23
|
+
return {
|
|
24
|
+
label: "{ … }",
|
|
25
|
+
kind: ICON.key,
|
|
26
|
+
detail: `options of this call — ${options.map((spec) => spec.name).join(", ")}`,
|
|
27
|
+
insertText: `{ ${inner} }`,
|
|
28
|
+
insertTextFormat: InsertTextFormat.Snippet,
|
|
29
|
+
textEdit: { range, newText: `{ ${inner} }` },
|
|
30
|
+
sortText: "0",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One key, written into a fresh map so it lands somewhere valid. */
|
|
35
|
+
function keyItem(spec: ParamSpec, range: Range): CompletionItem {
|
|
36
|
+
const text = `{ ${spec.name}: $1 }`;
|
|
37
|
+
return {
|
|
38
|
+
label: spec.name,
|
|
39
|
+
kind: ICON.key,
|
|
40
|
+
detail: spec.required ? `${spec.type} (required)` : spec.type,
|
|
41
|
+
documentation: spec.doc,
|
|
42
|
+
insertText: text,
|
|
43
|
+
insertTextFormat: InsertTextFormat.Snippet,
|
|
44
|
+
textEdit: { range, newText: text },
|
|
45
|
+
sortText: `1${spec.name}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type AstNode, isMember, prune, type Type } from "@venn-lang/core";
|
|
2
|
+
import { type CstNode, CstUtils, type LangiumDocument } from "langium";
|
|
3
|
+
|
|
4
|
+
export interface ReadFromArgs {
|
|
5
|
+
/** The node the cursor's dot was found against. */
|
|
6
|
+
host: AstNode;
|
|
7
|
+
/** Where the cursor is: what tells a finished member from one being typed. */
|
|
8
|
+
at: number;
|
|
9
|
+
document: LangiumDocument;
|
|
10
|
+
types: ReadonlyMap<object, Type>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The type on the left of a dot: what the members after it belong to.
|
|
15
|
+
*
|
|
16
|
+
* Three shapes sit here that look alike and are not: a member still being
|
|
17
|
+
* typed, a member already written, and a bracket that only groups. Each is read
|
|
18
|
+
* differently, which is why a plain `a.b.c.` is not the whole problem.
|
|
19
|
+
*/
|
|
20
|
+
export function receiverTypeAt(args: ReadFromArgs): Type | undefined {
|
|
21
|
+
const node = holderOf(args.host, args.at);
|
|
22
|
+
const found = args.types.get(node) ?? groupType(args);
|
|
23
|
+
return found && prune(found);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A member already written, told apart from one still being typed.
|
|
28
|
+
*
|
|
29
|
+
* `xs[0].ma▮` is half a member with no type of its own, so its receiver
|
|
30
|
+
* answers. `xs[0].entries.▮` is finished and its type is the list it gives
|
|
31
|
+
* back, so past the member's last character it answers for itself.
|
|
32
|
+
*/
|
|
33
|
+
function holderOf(host: AstNode, at: number): AstNode {
|
|
34
|
+
if (!isMember(host)) return host;
|
|
35
|
+
const end = host.$cstNode?.end;
|
|
36
|
+
return end !== undefined && end < at ? host : host.receiver;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What a grouping `(…)` holds: `(1 + 2).▮` is a number.
|
|
41
|
+
*
|
|
42
|
+
* Brackets that only group carry no node of their own, so the `)` belongs to
|
|
43
|
+
* the enclosing statement and the cursor lands on something untyped. A call or
|
|
44
|
+
* an index does have a node ending at its bracket, so this runs only when the
|
|
45
|
+
* direct reading comes up empty.
|
|
46
|
+
*/
|
|
47
|
+
function groupType(args: ReadFromArgs): Type | undefined {
|
|
48
|
+
const root = args.document.parseResult?.value?.$cstNode;
|
|
49
|
+
if (!root) return undefined;
|
|
50
|
+
const closing = before(root, args.at - 1);
|
|
51
|
+
if (closing?.text !== ")") return undefined;
|
|
52
|
+
const last = before(root, closing.offset - 1)?.astNode;
|
|
53
|
+
return last && outermostTyped({ from: last, end: closing.offset, types: args.types });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function before(root: CstNode, at: number): CstNode | undefined {
|
|
57
|
+
return CstUtils.findLeafNodeAtOffset(root, at) ?? CstUtils.findLeafNodeBeforeOffset(root, at);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The largest expression that still ends inside the brackets: `("a" + "b").`
|
|
62
|
+
* is the whole sum, not its last word. Only a node the checker gave a type to
|
|
63
|
+
* counts: the tree also holds an argument list and a call ending in the same
|
|
64
|
+
* place, and neither is a value anyone can read a member from.
|
|
65
|
+
*/
|
|
66
|
+
function outermostTyped(args: {
|
|
67
|
+
from: AstNode;
|
|
68
|
+
end: number;
|
|
69
|
+
types: ReadonlyMap<object, Type>;
|
|
70
|
+
}): Type | undefined {
|
|
71
|
+
let found: Type | undefined;
|
|
72
|
+
for (let node: AstNode | undefined = args.from; node; node = node.$container) {
|
|
73
|
+
if ((node.$cstNode?.end ?? args.end + 1) > args.end) break;
|
|
74
|
+
found = args.types.get(node) ?? found;
|
|
75
|
+
}
|
|
76
|
+
return found;
|
|
77
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { BUILTIN_TYPES, type Document, isTypeDecl } from "@venn-lang/core";
|
|
2
|
+
import type { CompletionItem, Range } from "vscode-languageserver";
|
|
3
|
+
import type { SymbolCatalog } from "../catalog/index.js";
|
|
4
|
+
import { ICON } from "./icons.js";
|
|
5
|
+
import { item } from "./items.js";
|
|
6
|
+
|
|
7
|
+
export interface TypeNameArgs {
|
|
8
|
+
root: Document | undefined;
|
|
9
|
+
catalog: SymbolCatalog;
|
|
10
|
+
/** Namespaces this file brought in; a type it cannot name is not offered. */
|
|
11
|
+
imported: readonly string[];
|
|
12
|
+
range: Range;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Every type that may be written here: the language's own, the ones this file
|
|
17
|
+
* declared, and the ones its imports publish, in that order.
|
|
18
|
+
*/
|
|
19
|
+
export function typeNameItems(args: TypeNameArgs): CompletionItem[] {
|
|
20
|
+
return [...builtins(args.range), ...declared(args), ...published(args)];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function builtins(range: Range): CompletionItem[] {
|
|
24
|
+
return Object.entries(BUILTIN_TYPES).map(([name, builtin]) => ({
|
|
25
|
+
...item({
|
|
26
|
+
label: name,
|
|
27
|
+
kind: ICON.builtinType,
|
|
28
|
+
range,
|
|
29
|
+
detail: "built in",
|
|
30
|
+
documentation: builtin.doc,
|
|
31
|
+
}),
|
|
32
|
+
sortText: `0${name}`,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What this file declared with `type`. */
|
|
37
|
+
function declared(args: TypeNameArgs): CompletionItem[] {
|
|
38
|
+
return (args.root?.decls ?? []).filter(isTypeDecl).map((decl) => ({
|
|
39
|
+
...item({
|
|
40
|
+
label: decl.name,
|
|
41
|
+
kind: ICON.type,
|
|
42
|
+
range: args.range,
|
|
43
|
+
detail: "declared here",
|
|
44
|
+
}),
|
|
45
|
+
sortText: `1${decl.name}`,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What the imported packages publish, written qualified, as they are used. */
|
|
50
|
+
function published(args: TypeNameArgs): CompletionItem[] {
|
|
51
|
+
return args.imported.flatMap((namespace) =>
|
|
52
|
+
args.catalog.typesIn(namespace).map((entry) => ({
|
|
53
|
+
...item({
|
|
54
|
+
label: `${entry.namespace}.${entry.name}`,
|
|
55
|
+
kind: ICON.type,
|
|
56
|
+
range: args.range,
|
|
57
|
+
detail: entry.package,
|
|
58
|
+
}),
|
|
59
|
+
sortText: `2${entry.namespace}.${entry.name}`,
|
|
60
|
+
})),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DecoratorDefinition } from "@venn-lang/core";
|
|
2
|
+
import { builtinDecorators } from "@venn-lang/runtime";
|
|
3
|
+
import { decoratorDoc } from "./builtin-docs.js";
|
|
4
|
+
import type { DecoInfo } from "./deco.types.js";
|
|
5
|
+
import { kindOf } from "./kind-of.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `@doc` is written like a decorator and read like one, but expansion never
|
|
9
|
+
* sees it: it is where a declaration's documentation lives when no `##` block
|
|
10
|
+
* does, and the editor is what reads it.
|
|
11
|
+
*/
|
|
12
|
+
const DOC: DecoInfo = {
|
|
13
|
+
name: "doc",
|
|
14
|
+
decorates: [],
|
|
15
|
+
signature: "@doc(text)",
|
|
16
|
+
doc: decoratorDoc("doc"),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The decorators every file has without asking: the ones the runtime ships.
|
|
21
|
+
* They are read through the same shape a declared `deco` produces, so the
|
|
22
|
+
* editor holds one notion of what a decorator is rather than two.
|
|
23
|
+
*/
|
|
24
|
+
export function builtinDecos(): DecoInfo[] {
|
|
25
|
+
return [...builtinDecorators.map(fromDefinition), DOC];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fromDefinition(decorator: DecoratorDefinition): DecoInfo {
|
|
29
|
+
return {
|
|
30
|
+
name: decorator.name,
|
|
31
|
+
decorates: (decorator.targets ?? []).map(kindOf),
|
|
32
|
+
signature: `@${decorator.name}`,
|
|
33
|
+
doc: decoratorDoc(decorator.name) ?? decorator.doc,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each decorator the language documents is for, per spec §08.
|
|
3
|
+
*
|
|
4
|
+
* Longer than the one-line `doc` a `DecoratorDefinition` carries, because a
|
|
5
|
+
* hover is where someone actually reads it. A name here that no decorator
|
|
6
|
+
* implements is still documented: the editor can explain `@load` long before
|
|
7
|
+
* `@venn-lang/load` contributes it.
|
|
8
|
+
*/
|
|
9
|
+
const DOCS: Record<string, string> = {
|
|
10
|
+
doc: "Documentation shown in the editor and the node tooltip.",
|
|
11
|
+
timeout: "Abort the flow or step after a duration, e.g. `@timeout(90s)`.",
|
|
12
|
+
retry: "Re-run on failure with backoff, e.g. `@retry(2, { backoff: 500ms })`.",
|
|
13
|
+
skip: "Skip this flow or step, optionally conditionally.",
|
|
14
|
+
only: "Focus: run only the annotated flows.",
|
|
15
|
+
serial: "Forbid concurrent execution with sibling flows.",
|
|
16
|
+
lock: 'Named mutex held across workers, e.g. `@lock("orders")`.',
|
|
17
|
+
scope: "Resource lifetime: `suite`, `worker`, `flow` or `step`.",
|
|
18
|
+
flaky: "Declared flakiness tolerance, e.g. `@flaky(ratio: 0.05)`.",
|
|
19
|
+
tags: "Tags matched by the runner's `--tags` filter.",
|
|
20
|
+
load: "Run the flow as a load test (from `@venn-lang/load`).",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** The prose for a decorator the language documents, if it documents one. */
|
|
24
|
+
export function decoratorDoc(name: string): string | undefined {
|
|
25
|
+
return DOCS[name];
|
|
26
|
+
}
|