@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,416 @@
|
|
|
1
|
+
import { type AstNode, type Document, isUseDecl } from "@venn-lang/core";
|
|
2
|
+
import type { LangiumDocument, LangiumDocuments } from "langium";
|
|
3
|
+
import type { CompletionProvider } from "langium/lsp";
|
|
4
|
+
import type {
|
|
5
|
+
CompletionItem,
|
|
6
|
+
CompletionList,
|
|
7
|
+
CompletionParams,
|
|
8
|
+
Position,
|
|
9
|
+
Range,
|
|
10
|
+
} from "vscode-languageserver";
|
|
11
|
+
import type { SymbolCatalog } from "../catalog/index.js";
|
|
12
|
+
import { decosInScope } from "../deco/index.js";
|
|
13
|
+
import {
|
|
14
|
+
exportedNames,
|
|
15
|
+
hostAt,
|
|
16
|
+
importedFragments,
|
|
17
|
+
importedModules,
|
|
18
|
+
namesInScope,
|
|
19
|
+
type ScopedName,
|
|
20
|
+
} from "../document/index.js";
|
|
21
|
+
import { envVars } from "../env/index.js";
|
|
22
|
+
import type { VennServices } from "../services/lsp.types.js";
|
|
23
|
+
import {
|
|
24
|
+
activeArg,
|
|
25
|
+
type CallShape,
|
|
26
|
+
callShape,
|
|
27
|
+
enclosingBareCall,
|
|
28
|
+
enclosingCall,
|
|
29
|
+
enclosingMatcher,
|
|
30
|
+
enclosingParenCall,
|
|
31
|
+
matcherShape,
|
|
32
|
+
type ShownArg,
|
|
33
|
+
shapeAt,
|
|
34
|
+
} from "../signature/index.js";
|
|
35
|
+
import type { TypeService } from "../types/index.js";
|
|
36
|
+
import type { ImportResolver } from "../workspace/index.js";
|
|
37
|
+
import { argumentItems, boundItems } from "./argument-items.js";
|
|
38
|
+
import type { CompletionContext } from "./completion.types.js";
|
|
39
|
+
import { contextAt } from "./context.js";
|
|
40
|
+
import {
|
|
41
|
+
actionItems,
|
|
42
|
+
annotationItems,
|
|
43
|
+
envItems,
|
|
44
|
+
exportItems,
|
|
45
|
+
fragmentItems,
|
|
46
|
+
matcherItems,
|
|
47
|
+
openingItems,
|
|
48
|
+
optionItems,
|
|
49
|
+
packageItems,
|
|
50
|
+
pathItems,
|
|
51
|
+
typeItems,
|
|
52
|
+
} from "./items.js";
|
|
53
|
+
import { memberItems, membersOfNode } from "./member-items.js";
|
|
54
|
+
import { modulePaths } from "./module-paths.js";
|
|
55
|
+
import { optionsMapItems } from "./options-map-items.js";
|
|
56
|
+
import { typeNameItems } from "./type-name-items.js";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Context-aware completion, driven by the text before the cursor.
|
|
60
|
+
*
|
|
61
|
+
* `contextAt` classifies the cursor, and each context has exactly one source of
|
|
62
|
+
* items: a namespace's verbs, an options map's keys, the names in scope.
|
|
63
|
+
*/
|
|
64
|
+
export class VennCompletionProvider implements CompletionProvider {
|
|
65
|
+
private readonly catalog: SymbolCatalog;
|
|
66
|
+
private readonly documents: LangiumDocuments;
|
|
67
|
+
private readonly imports: ImportResolver;
|
|
68
|
+
private readonly types: TypeService;
|
|
69
|
+
|
|
70
|
+
constructor(services: VennServices) {
|
|
71
|
+
this.catalog = services.catalog;
|
|
72
|
+
this.documents = services.shared.workspace.LangiumDocuments;
|
|
73
|
+
this.imports = services.imports;
|
|
74
|
+
this.types = services.types;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getCompletion(
|
|
78
|
+
document: LangiumDocument,
|
|
79
|
+
params: CompletionParams,
|
|
80
|
+
): Promise<CompletionList> {
|
|
81
|
+
const line = lineAt(document, params.position);
|
|
82
|
+
const text = document.textDocument.getText();
|
|
83
|
+
const offset = document.textDocument.offsetAt(params.position);
|
|
84
|
+
const context = contextAt({
|
|
85
|
+
prefix: line.slice(0, params.position.character),
|
|
86
|
+
line,
|
|
87
|
+
before: text.slice(0, offset),
|
|
88
|
+
});
|
|
89
|
+
const range = rangeOf(context, params.position);
|
|
90
|
+
return { isIncomplete: false, items: await this.itemsFor(context, range, document, offset) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* What follows a dot. A name the file bound wins over a plugin namespace, the
|
|
95
|
+
* same rule the evaluator and the highlighter follow, so a variable named
|
|
96
|
+
* `auth` completes as its own value even when `@venn-lang/auth` is loaded.
|
|
97
|
+
*/
|
|
98
|
+
private afterDot(
|
|
99
|
+
receiver: string,
|
|
100
|
+
range: Range,
|
|
101
|
+
document: LangiumDocument,
|
|
102
|
+
offset: number,
|
|
103
|
+
): CompletionItem[] {
|
|
104
|
+
const types = this.types.of(document);
|
|
105
|
+
const host = hostAt({ document, offset, types, prefer: "before" });
|
|
106
|
+
const members = host
|
|
107
|
+
? memberItems({ receiver, host, document, types: this.types, range })
|
|
108
|
+
: undefined;
|
|
109
|
+
if (members?.length) return members;
|
|
110
|
+
if (receiver === "env") return this.env(document, range);
|
|
111
|
+
// A namespace answers with what it does and with what it deals in.
|
|
112
|
+
return [
|
|
113
|
+
...actionItems(receiver, this.catalog, range),
|
|
114
|
+
...typeItems(receiver, this.catalog, range),
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* What follows a dot no name can precede: `(1234.567).`, `f(x).`, `xs[0].`.
|
|
120
|
+
* The receiver has no text to look up, so its inferred type answers.
|
|
121
|
+
*/
|
|
122
|
+
private afterExpression(
|
|
123
|
+
range: Range,
|
|
124
|
+
document: LangiumDocument,
|
|
125
|
+
offset: number,
|
|
126
|
+
): CompletionItem[] {
|
|
127
|
+
const types = this.types.of(document);
|
|
128
|
+
const host = hostAt({ document, offset, types, prefer: "before" });
|
|
129
|
+
return host ? membersOfNode({ host, at: offset, document, types: this.types, range }) : [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private env(document: LangiumDocument, range: Range): CompletionItem[] {
|
|
133
|
+
const uri = document.uri;
|
|
134
|
+
return envItems(envVars(this.imports.env(uri), this.imports.envDocs(uri)), range);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async itemsFor(
|
|
138
|
+
context: CompletionContext,
|
|
139
|
+
range: Range,
|
|
140
|
+
document: LangiumDocument,
|
|
141
|
+
offset: number,
|
|
142
|
+
): Promise<CompletionItem[]> {
|
|
143
|
+
if (context.kind === "package") return packageItems(this.catalog, range);
|
|
144
|
+
if (context.kind === "modulePath") return this.paths(context.partial, range, document);
|
|
145
|
+
if (context.kind === "importName") return this.exports(context.path, range, document);
|
|
146
|
+
if (context.kind === "action") return this.afterDot(context.receiver, range, document, offset);
|
|
147
|
+
if (context.kind === "member") return this.afterExpression(range, document, offset);
|
|
148
|
+
if (context.kind === "annotation") return this.decorators(document, range);
|
|
149
|
+
if (context.kind === "fragment") return this.fragments(range, document);
|
|
150
|
+
if (context.kind === "matcher") return matcherItems(this.catalog, range);
|
|
151
|
+
if (context.kind === "optionKey") {
|
|
152
|
+
return optionItems({ target: context.target, catalog: this.catalog, range });
|
|
153
|
+
}
|
|
154
|
+
if (context.kind === "typeName") return this.typeNames(range, document);
|
|
155
|
+
if (context.kind === "argument") return this.argument(context, range, document, offset);
|
|
156
|
+
return this.fallback(range, document, offset);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Every name in scope, with an imported one drawn as what it really is.
|
|
161
|
+
*
|
|
162
|
+
* `namesInScope` reads this file's tree, so it can say only that a name came
|
|
163
|
+
* from an import. Its kind lives in the file it came from, which is in reach
|
|
164
|
+
* here, so an imported fragment is drawn as a fragment rather than as an
|
|
165
|
+
* anonymous binding.
|
|
166
|
+
*/
|
|
167
|
+
private scopeNames(host: AstNode, at: number, document: LangiumDocument): ScopedName[] {
|
|
168
|
+
const found = namesInScope(host, at);
|
|
169
|
+
if (!found.some((one) => one.origin === "import")) return found;
|
|
170
|
+
const root = rootOf(document);
|
|
171
|
+
const known = root ? this.publishedTo(root, document) : new Map<string, string>();
|
|
172
|
+
return found.map((one) =>
|
|
173
|
+
one.origin === "import" ? { ...one, origin: known.get(one.name) ?? one.origin } : one,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** What each name this file imported was declared as, in the file it came from. */
|
|
178
|
+
private publishedTo(root: Document, document: LangiumDocument): Map<string, string> {
|
|
179
|
+
const graph = importedModules({
|
|
180
|
+
root,
|
|
181
|
+
uri: document.uri,
|
|
182
|
+
documents: this.documents,
|
|
183
|
+
imports: this.imports,
|
|
184
|
+
});
|
|
185
|
+
const known = new Map<string, string>();
|
|
186
|
+
for (const module of graph.modules.values()) {
|
|
187
|
+
for (const each of exportedNames(module)) known.set(each.name, each.origin);
|
|
188
|
+
}
|
|
189
|
+
return known;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* `run ▮`: the fragments this file declared, and the ones it imported.
|
|
194
|
+
*
|
|
195
|
+
* `run` can only invoke a fragment, so an imported name is offered only once
|
|
196
|
+
* the file it came from confirms it is one.
|
|
197
|
+
*/
|
|
198
|
+
private fragments(range: Range, document: LangiumDocument): CompletionItem[] {
|
|
199
|
+
const root = rootOf(document);
|
|
200
|
+
if (!root) return [];
|
|
201
|
+
const graph = importedModules({
|
|
202
|
+
root,
|
|
203
|
+
uri: document.uri,
|
|
204
|
+
documents: this.documents,
|
|
205
|
+
imports: this.imports,
|
|
206
|
+
});
|
|
207
|
+
const imported = importedFragments({ document: root, uri: document.uri.toString(), graph });
|
|
208
|
+
return fragmentItems({ document: root, imported, range });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** `id: ▮`: the language's own types, this file's, and its imports'. */
|
|
212
|
+
private typeNames(range: Range, document: LangiumDocument): CompletionItem[] {
|
|
213
|
+
const root = rootOf(document);
|
|
214
|
+
return typeNameItems({
|
|
215
|
+
root,
|
|
216
|
+
catalog: this.catalog,
|
|
217
|
+
imported: root ? namespacesUsed(root, this.catalog) : [],
|
|
218
|
+
range,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The last two shapes a cursor can be in before it is simply a new statement:
|
|
224
|
+
* inside a call's brackets, or after a matcher word.
|
|
225
|
+
*/
|
|
226
|
+
private fallback(range: Range, document: LangiumDocument, offset: number): CompletionItem[] {
|
|
227
|
+
return (
|
|
228
|
+
this.inBrackets(range, document, offset) ??
|
|
229
|
+
this.afterBareCall(range, document, offset) ??
|
|
230
|
+
this.afterMatcher(range, document, offset) ??
|
|
231
|
+
this.opening(range, document, offset)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* A fresh statement: the names already bound, then the words that open one.
|
|
237
|
+
*
|
|
238
|
+
* Calling a function is itself a statement, so bound names belong here. They
|
|
239
|
+
* come first because the language's own vocabulary is always available.
|
|
240
|
+
*/
|
|
241
|
+
private opening(range: Range, document: LangiumDocument, offset: number): CompletionItem[] {
|
|
242
|
+
const host = hostAt({ document, offset, types: this.types.of(document), prefer: "before" });
|
|
243
|
+
const bound = host ? boundItems(this.scopeNames(host, offset, document), range) : [];
|
|
244
|
+
return [...bound, ...openingItems(this.catalog, range)];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* `const r = http.get "u" ▮`: a bracket-less call bound to a name.
|
|
249
|
+
*
|
|
250
|
+
* The `argument` context reads the head of the line, which here is `const`
|
|
251
|
+
* rather than the verb. Same words deserve the same help, so it is caught
|
|
252
|
+
* separately.
|
|
253
|
+
*/
|
|
254
|
+
private afterBareCall(
|
|
255
|
+
range: Range,
|
|
256
|
+
document: LangiumDocument,
|
|
257
|
+
offset: number,
|
|
258
|
+
): CompletionItem[] | undefined {
|
|
259
|
+
const call = enclosingBareCall(document, offset);
|
|
260
|
+
const shape = call && callShape(call.target, this.catalog);
|
|
261
|
+
if (!call || !shape) return undefined;
|
|
262
|
+
return this.dueAt({
|
|
263
|
+
active: activeArg(call, offset),
|
|
264
|
+
shape,
|
|
265
|
+
host: call.node,
|
|
266
|
+
range,
|
|
267
|
+
document,
|
|
268
|
+
at: offset,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** What may go in this position: a value, or the options map that ends the call. */
|
|
273
|
+
private dueAt(args: {
|
|
274
|
+
active: number;
|
|
275
|
+
shape: CallShape;
|
|
276
|
+
host: AstNode;
|
|
277
|
+
range: Range;
|
|
278
|
+
document: LangiumDocument;
|
|
279
|
+
at: number;
|
|
280
|
+
}): CompletionItem[] {
|
|
281
|
+
const { active, shape } = args;
|
|
282
|
+
if (active >= shape.args.length && shape.options.length > 0) {
|
|
283
|
+
return optionsMapItems(shape.options, args.range);
|
|
284
|
+
}
|
|
285
|
+
return this.valuesFor({ ...args, expects: shape.args[active] });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** `expect res contains ▮`: the value the check is waiting for. */
|
|
289
|
+
private afterMatcher(
|
|
290
|
+
range: Range,
|
|
291
|
+
document: LangiumDocument,
|
|
292
|
+
offset: number,
|
|
293
|
+
): CompletionItem[] | undefined {
|
|
294
|
+
const clause = enclosingMatcher(document, offset);
|
|
295
|
+
if (!clause) return undefined;
|
|
296
|
+
const shape = matcherShape(clause.name, this.catalog);
|
|
297
|
+
const expects = shape?.args[activeArg(clause, offset)];
|
|
298
|
+
return this.valuesFor({ host: clause, expects, range, document, at: offset });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* `saudacao(▮)`, `fmt.json(x, ▮)`: inside a bracketed call.
|
|
303
|
+
*
|
|
304
|
+
* Reached last, before offering a fresh statement. Everything more specific
|
|
305
|
+
* (a dot, an options key, a bare argument) has already had its turn, so what
|
|
306
|
+
* is left inside brackets is a value.
|
|
307
|
+
*/
|
|
308
|
+
private inBrackets(
|
|
309
|
+
range: Range,
|
|
310
|
+
document: LangiumDocument,
|
|
311
|
+
offset: number,
|
|
312
|
+
): CompletionItem[] | undefined {
|
|
313
|
+
const found = enclosingParenCall(document, offset);
|
|
314
|
+
if (!found?.host) return undefined;
|
|
315
|
+
const shape = shapeAt({ ...found, document, catalog: this.catalog, types: this.types });
|
|
316
|
+
if (!shape)
|
|
317
|
+
return this.valuesFor({ host: found.host, expects: undefined, range, document, at: offset });
|
|
318
|
+
return this.dueAt({
|
|
319
|
+
active: found.active,
|
|
320
|
+
shape,
|
|
321
|
+
host: found.host,
|
|
322
|
+
range,
|
|
323
|
+
document,
|
|
324
|
+
at: offset,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* `http.on ▮`: what the program already holds, before what the stdlib offers.
|
|
330
|
+
* A name the text took for a verb but the language does not know is not an
|
|
331
|
+
* argument position at all, so that falls back to a fresh statement.
|
|
332
|
+
*/
|
|
333
|
+
private argument(
|
|
334
|
+
context: { target: string; from: number },
|
|
335
|
+
range: Range,
|
|
336
|
+
document: LangiumDocument,
|
|
337
|
+
offset: number,
|
|
338
|
+
): CompletionItem[] {
|
|
339
|
+
const shape = callShape(context.target, this.catalog);
|
|
340
|
+
const call = enclosingCall(document, offset);
|
|
341
|
+
// The text took the head of the line for a verb and the language disagrees:
|
|
342
|
+
// `const t = saudacao(▮)` reads that way and is a bracketed call.
|
|
343
|
+
if (!shape || !call) {
|
|
344
|
+
return this.fallback(range, document, offset);
|
|
345
|
+
}
|
|
346
|
+
const expects = shape.args[activeArg(call, offset)];
|
|
347
|
+
return this.valuesFor({ host: call, expects, range, document, at: offset });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** What the program holds that could go here, the fitting ones first. */
|
|
351
|
+
private valuesFor(args: {
|
|
352
|
+
host: AstNode;
|
|
353
|
+
expects: ShownArg | undefined;
|
|
354
|
+
range: Range;
|
|
355
|
+
document: LangiumDocument;
|
|
356
|
+
at: number;
|
|
357
|
+
}): CompletionItem[] {
|
|
358
|
+
return argumentItems({
|
|
359
|
+
names: this.scopeNames(args.host, args.at, args.document),
|
|
360
|
+
types: this.types.of(args.document).types,
|
|
361
|
+
expects: args.expects,
|
|
362
|
+
catalog: this.catalog,
|
|
363
|
+
range: args.range,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The built-ins, the `deco`s this file declares, and the `pub deco`s it imported.
|
|
368
|
+
private async decorators(document: LangiumDocument, range: Range): Promise<CompletionItem[]> {
|
|
369
|
+
const scope = { document, documents: this.documents, imports: this.imports };
|
|
370
|
+
return annotationItems(await decosInScope(scope), range);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private paths(partial: string, range: Range, document: LangiumDocument): CompletionItem[] {
|
|
374
|
+
const paths = modulePaths({ partial, base: document.uri, imports: this.imports });
|
|
375
|
+
return pathItems(paths, range);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Only what the imported module marks `pub` may be named inside `import { … }`.
|
|
379
|
+
private async exports(
|
|
380
|
+
path: string | undefined,
|
|
381
|
+
range: Range,
|
|
382
|
+
document: LangiumDocument,
|
|
383
|
+
): Promise<CompletionItem[]> {
|
|
384
|
+
if (!path) return [];
|
|
385
|
+
const uri = this.imports.resolve(path, document.uri);
|
|
386
|
+
const target = await this.documents.getOrCreateDocument(uri).catch(() => undefined);
|
|
387
|
+
const root = target?.parseResult?.value as Document | undefined;
|
|
388
|
+
return root ? exportItems(exportedNames(root), range) : [];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function rangeOf(context: CompletionContext, position: Position): Range {
|
|
393
|
+
return { start: { line: position.line, character: context.from }, end: position };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function rootOf(document: LangiumDocument): Document | undefined {
|
|
397
|
+
return document.parseResult?.value as Document | undefined;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function lineAt(document: LangiumDocument, position: Position): string {
|
|
401
|
+
const text = document.textDocument.getText();
|
|
402
|
+
const start = document.textDocument.offsetAt({ line: position.line, character: 0 });
|
|
403
|
+
const end = text.indexOf("\n", start);
|
|
404
|
+
return text.slice(start, end < 0 ? text.length : end);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The namespaces this file brought in; a type it cannot name is not offered. */
|
|
408
|
+
function namespacesUsed(root: Document, catalog: SymbolCatalog): string[] {
|
|
409
|
+
const names: string[] = [];
|
|
410
|
+
for (const decl of root.imports) {
|
|
411
|
+
if (!isUseDecl(decl)) continue;
|
|
412
|
+
const namespace = decl.alias ?? catalog.namespaceOfPackage(decl.pkg);
|
|
413
|
+
if (namespace) names.push(namespace);
|
|
414
|
+
}
|
|
415
|
+
return names;
|
|
416
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the cursor sits, and from which character the completion should
|
|
3
|
+
* replace. Without that range VS Code falls back to its own word rules, which
|
|
4
|
+
* break on `@` and `/`: accepting `@venn-lang/http` inside `"@venn-lang/"` would paste
|
|
5
|
+
* the prefix twice.
|
|
6
|
+
*/
|
|
7
|
+
export type CompletionContext =
|
|
8
|
+
| { kind: "package"; from: number }
|
|
9
|
+
| { kind: "modulePath"; from: number; partial: string }
|
|
10
|
+
| { kind: "importName"; from: number; path?: string }
|
|
11
|
+
/** After a dot. `receiver` is the whole path before it: a namespace or a value. */
|
|
12
|
+
| { kind: "action"; from: number; receiver: string }
|
|
13
|
+
/** After a dot whose receiver is not a name: the AST node has to answer. */
|
|
14
|
+
| { kind: "member"; from: number }
|
|
15
|
+
| { kind: "annotation"; from: number }
|
|
16
|
+
| { kind: "fragment"; from: number }
|
|
17
|
+
| { kind: "matcher"; from: number }
|
|
18
|
+
/** Inside the `{ … }` of a call; `target` is the action whose keys to offer. */
|
|
19
|
+
| { kind: "optionKey"; from: number; target: string }
|
|
20
|
+
/**
|
|
21
|
+
* After a verb and a space: `http.on ▮`. Whether `target` really is a verb is
|
|
22
|
+
* not a question text can answer, so the provider checks and falls back to a
|
|
23
|
+
* plain statement when nothing in the language answers to that name.
|
|
24
|
+
*/
|
|
25
|
+
| { kind: "argument"; from: number; target: string }
|
|
26
|
+
/** Where a type goes: after the `:` of a field, a parameter or a binding. */
|
|
27
|
+
| { kind: "typeName"; from: number }
|
|
28
|
+
| { kind: "statement"; from: number };
|
|
29
|
+
|
|
30
|
+
/** The text the classifier reads: the current line, and everything before the cursor. */
|
|
31
|
+
export interface CursorText {
|
|
32
|
+
/** The current line up to the cursor; `from` is a column on this line. */
|
|
33
|
+
prefix: string;
|
|
34
|
+
/** The whole current line. */
|
|
35
|
+
line: string;
|
|
36
|
+
/** The whole document up to the cursor; an options map spans lines. */
|
|
37
|
+
before: string;
|
|
38
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { CompletionContext, CursorText } from "./completion.types.js";
|
|
2
|
+
|
|
3
|
+
const PACKAGE = /\buse\s+"([^"]*)$/;
|
|
4
|
+
const MODULE_PATH = /\bfrom\s+"([^"]*)$/;
|
|
5
|
+
const IMPORT_NAME = /\bimport\s*\{([^}]*)$/;
|
|
6
|
+
/**
|
|
7
|
+
* The whole dotted receiver, so `cfg.server.` offers what `cfg.server` holds.
|
|
8
|
+
*
|
|
9
|
+
* The lookbehind makes the match start at a token boundary. Without it the
|
|
10
|
+
* receiver could be found inside something else: `_` opens an identifier, so
|
|
11
|
+
* `1_000_000.` would take `_000_000` for its receiver and offer nothing.
|
|
12
|
+
*/
|
|
13
|
+
const ACTION = /(?<![\w.])([A-Za-z_]\w*(?:\.\w+)*)\.(\w*)$/;
|
|
14
|
+
const ANNOTATION = /@(\w*)$/;
|
|
15
|
+
const FRAGMENT = /\brun\s+(\w*)$/;
|
|
16
|
+
const MATCHER = /\bexpect\b\s+\S+\s+(\w*)$/;
|
|
17
|
+
const WORD = /(\w*)$/;
|
|
18
|
+
/**
|
|
19
|
+
* Where a type goes: after the `:` of a field, a parameter or a binding, after
|
|
20
|
+
* the `->` of a declared return, or after the `|` of a union.
|
|
21
|
+
*
|
|
22
|
+
* A name has to sit before the colon, which is what tells a type annotation
|
|
23
|
+
* from a map entry: `{ port: ▮ }` holds a value, `port: ▮` in a `type` block
|
|
24
|
+
* holds a type. The brace is the difference, and the caller has already
|
|
25
|
+
* classified an open one.
|
|
26
|
+
*/
|
|
27
|
+
const TYPE_POSITION = /(?:^|[^:])(?::|->|\|)\s*([A-Za-z_][\w.]*)?$/;
|
|
28
|
+
/**
|
|
29
|
+
* A verb, a space, and whatever is being written after it: `http.on ▮`,
|
|
30
|
+
* `http.on api ▮`, `print ▮`. The leading anchor keeps this to the head of a
|
|
31
|
+
* statement, so `1 + ▮` and the tail of an expression are not swept in.
|
|
32
|
+
*/
|
|
33
|
+
const ARGUMENT = /^\s*([A-Za-z_]\w*(?:\.\w+)*)\s+[^{}]*?(\w*)$/;
|
|
34
|
+
const FROM_PATH = /\bfrom\s+"([^"]+)"/;
|
|
35
|
+
/**
|
|
36
|
+
* A dot whose receiver no path can name: `1234.567.round`, `f(x).len`,
|
|
37
|
+
* `xs[0].name`, `"ab".upper`. Tried after {@link ACTION}, so a receiver that is
|
|
38
|
+
* a name goes there first and this catches the rest: a digit, a closing
|
|
39
|
+
* bracket, a quote. There is nothing to look up by text, so the node under the
|
|
40
|
+
* cursor answers instead.
|
|
41
|
+
*/
|
|
42
|
+
const MEMBER = /[\w)\]"']\.(\w*)$/;
|
|
43
|
+
|
|
44
|
+
/** The dotted path that owns the `{` we are inside of, with no brace between. */
|
|
45
|
+
const OPTION_OWNER = /([A-Za-z_]\w*(?:\.\w+)+)[^{}]*$/;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Classify the cursor from the text before it. String contexts are tested first:
|
|
49
|
+
* a module path like `#shared/auth.vn` also looks like `namespace.action`.
|
|
50
|
+
*
|
|
51
|
+
* @param text The line, the prefix up to the cursor and the document before it.
|
|
52
|
+
* @returns The narrowest context that matches, or `statement` when none does.
|
|
53
|
+
*/
|
|
54
|
+
export function contextAt(text: CursorText): CompletionContext {
|
|
55
|
+
const { prefix, line } = text;
|
|
56
|
+
const inString = stringContext(prefix, line);
|
|
57
|
+
if (inString) return inString;
|
|
58
|
+
const action = ACTION.exec(prefix);
|
|
59
|
+
if (action?.[1]) return { kind: "action", receiver: action[1], from: back(prefix, action[2]) };
|
|
60
|
+
const member = MEMBER.exec(prefix);
|
|
61
|
+
if (member) return { kind: "member", from: back(prefix, member[1]) };
|
|
62
|
+
const annotation = ANNOTATION.exec(prefix);
|
|
63
|
+
if (annotation) return { kind: "annotation", from: back(prefix, annotation[1]) };
|
|
64
|
+
const fragment = FRAGMENT.exec(prefix);
|
|
65
|
+
if (fragment) return { kind: "fragment", from: back(prefix, fragment[1]) };
|
|
66
|
+
const matcher = MATCHER.exec(prefix);
|
|
67
|
+
if (matcher) return { kind: "matcher", from: back(prefix, matcher[1]) };
|
|
68
|
+
return (
|
|
69
|
+
optionContext(text) ??
|
|
70
|
+
typeContext(text) ??
|
|
71
|
+
argumentContext(prefix) ?? { kind: "statement", from: back(prefix, WORD.exec(prefix)?.[1]) }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `id: ▮`: a type is due.
|
|
77
|
+
*
|
|
78
|
+
* Only outside an options map, which the caller has already ruled out: inside
|
|
79
|
+
* `{ … }` a colon introduces a value, and the two look identical to a regex.
|
|
80
|
+
*/
|
|
81
|
+
function typeContext(text: CursorText): CompletionContext | undefined {
|
|
82
|
+
const found = TYPE_POSITION.exec(text.prefix);
|
|
83
|
+
if (!found) return undefined;
|
|
84
|
+
return { kind: "typeName", from: back(text.prefix, found[1]) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** `http.on ▮`: the head of the line is a verb, and an argument is due. */
|
|
88
|
+
function argumentContext(prefix: string): CompletionContext | undefined {
|
|
89
|
+
const found = ARGUMENT.exec(prefix);
|
|
90
|
+
if (!found?.[1]) return undefined;
|
|
91
|
+
return { kind: "argument", target: found[1], from: back(prefix, found[2]) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `http.post url { … }`: offer the keys that call accepts. Only the map that
|
|
96
|
+
* belongs to the call qualifies: a nested `{ … }` has a shape of its own that
|
|
97
|
+
* the schema does not describe, so nothing is offered there rather than
|
|
98
|
+
* something wrong.
|
|
99
|
+
*/
|
|
100
|
+
function optionContext(text: CursorText): CompletionContext | undefined {
|
|
101
|
+
const open = innermostOpenBrace(text.before);
|
|
102
|
+
if (open < 0) return undefined;
|
|
103
|
+
const target = OPTION_OWNER.exec(text.before.slice(0, open))?.[1];
|
|
104
|
+
if (!target) return undefined;
|
|
105
|
+
return { kind: "optionKey", target, from: back(text.prefix, WORD.exec(text.prefix)?.[1]) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The `{` still open at the cursor, scanning backwards. */
|
|
109
|
+
function innermostOpenBrace(before: string): number {
|
|
110
|
+
let depth = 0;
|
|
111
|
+
for (let index = before.length - 1; index >= 0; index -= 1) {
|
|
112
|
+
if (before[index] === "}") depth += 1;
|
|
113
|
+
else if (before[index] === "{") {
|
|
114
|
+
if (depth === 0) return index;
|
|
115
|
+
depth -= 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return -1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function stringContext(prefix: string, line: string): CompletionContext | undefined {
|
|
122
|
+
const pkg = PACKAGE.exec(prefix);
|
|
123
|
+
if (pkg) return { kind: "package", from: back(prefix, pkg[1]) };
|
|
124
|
+
const path = MODULE_PATH.exec(prefix);
|
|
125
|
+
if (path) return { kind: "modulePath", from: back(prefix, path[1]), partial: path[1] ?? "" };
|
|
126
|
+
const named = IMPORT_NAME.exec(prefix);
|
|
127
|
+
if (named)
|
|
128
|
+
return { kind: "importName", from: back(prefix, lastName(named[1])), path: pathOf(line) };
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The name being typed inside `{ a, b| }`: everything after the last comma.
|
|
133
|
+
function lastName(inside: string | undefined): string {
|
|
134
|
+
const tail = (inside ?? "").split(",").pop() ?? "";
|
|
135
|
+
return tail.trimStart();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function pathOf(line: string): string | undefined {
|
|
139
|
+
return FROM_PATH.exec(line)?.[1];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function back(prefix: string, partial: string | undefined): number {
|
|
143
|
+
return prefix.length - (partial?.length ?? 0);
|
|
144
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { CompletionItemKind } from "vscode-languageserver";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One icon per kind of thing, decided in one place so the item builders cannot
|
|
5
|
+
* disagree about what a published type or a bound name looks like.
|
|
6
|
+
*
|
|
7
|
+
* The rule the icons follow is what the reader is about to write: a `Method`
|
|
8
|
+
* needs brackets, a `Property` is computed and read bare, a `Field` is data
|
|
9
|
+
* sitting there. Everything else names the sort of declaration it came from.
|
|
10
|
+
*/
|
|
11
|
+
export interface IconTable {
|
|
12
|
+
readonly constant: CompletionItemKind;
|
|
13
|
+
readonly variable: CompletionItemKind;
|
|
14
|
+
readonly callable: CompletionItemKind;
|
|
15
|
+
readonly fragment: CompletionItemKind;
|
|
16
|
+
readonly verb: CompletionItemKind;
|
|
17
|
+
readonly matcher: CompletionItemKind;
|
|
18
|
+
readonly decorator: CompletionItemKind;
|
|
19
|
+
readonly namespace: CompletionItemKind;
|
|
20
|
+
readonly type: CompletionItemKind;
|
|
21
|
+
readonly builtinType: CompletionItemKind;
|
|
22
|
+
readonly key: CompletionItemKind;
|
|
23
|
+
readonly computed: CompletionItemKind;
|
|
24
|
+
readonly method: CompletionItemKind;
|
|
25
|
+
readonly env: CompletionItemKind;
|
|
26
|
+
readonly path: CompletionItemKind;
|
|
27
|
+
readonly keyword: CompletionItemKind;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ICON: IconTable = {
|
|
31
|
+
/** `const x = …`: bound once, never reassigned. */
|
|
32
|
+
constant: CompletionItemKind.Constant,
|
|
33
|
+
/** `let x = …`, a parameter, a loop variable. */
|
|
34
|
+
variable: CompletionItemKind.Variable,
|
|
35
|
+
/** A `fn`: called for a value, in an expression. */
|
|
36
|
+
callable: CompletionItemKind.Function,
|
|
37
|
+
/**
|
|
38
|
+
* A `fragment`: a named piece of a flow, invoked with `run`.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately not the icon a function carries. A function gives back a
|
|
41
|
+
* value; a fragment gives back steps that the report records and that can
|
|
42
|
+
* fail. The icon has to separate them while the reader is still choosing.
|
|
43
|
+
*/
|
|
44
|
+
fragment: CompletionItemKind.Snippet,
|
|
45
|
+
/** A verb a plugin contributes: `http.get`. */
|
|
46
|
+
verb: CompletionItemKind.Function,
|
|
47
|
+
/** A word usable after `expect`: `contains`, `oneOf`. */
|
|
48
|
+
matcher: CompletionItemKind.Function,
|
|
49
|
+
/** A `deco`, applied with `@`. */
|
|
50
|
+
decorator: CompletionItemKind.Function,
|
|
51
|
+
/** A plugin namespace, or a package to `use`. */
|
|
52
|
+
namespace: CompletionItemKind.Module,
|
|
53
|
+
/** A type: declared here, published by a package, or a `type` alias. */
|
|
54
|
+
type: CompletionItemKind.Struct,
|
|
55
|
+
/** One of the language's own types: `string`, `duration`. */
|
|
56
|
+
builtinType: CompletionItemKind.Keyword,
|
|
57
|
+
/** A key of a map someone writes: an option, a field of a record. */
|
|
58
|
+
key: CompletionItemKind.Field,
|
|
59
|
+
/** Read bare, computed each time: `xs.len`, `m.keys`. */
|
|
60
|
+
computed: CompletionItemKind.Property,
|
|
61
|
+
/** Called with brackets: `m.get(k)`, `xs.map(f)`. */
|
|
62
|
+
method: CompletionItemKind.Method,
|
|
63
|
+
/** A variable the manifest declares. */
|
|
64
|
+
env: CompletionItemKind.Constant,
|
|
65
|
+
/** A file path, offered inside an import string. */
|
|
66
|
+
path: CompletionItemKind.File,
|
|
67
|
+
/** A keyword of the language: `step`, `forEach`. */
|
|
68
|
+
keyword: CompletionItemKind.Keyword,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The icon for a name the program bound, from how it was bound.
|
|
73
|
+
*
|
|
74
|
+
* `origin` is what {@link ScopedName} recorded when the name was found, so the
|
|
75
|
+
* editor draws a `const` apart from a `let` apart from a `fn` without anyone
|
|
76
|
+
* having to look the declaration up a second time.
|
|
77
|
+
*/
|
|
78
|
+
export function iconForOrigin(origin: string): CompletionItemKind {
|
|
79
|
+
if (origin === "const" || origin === "dataset") return ICON.constant;
|
|
80
|
+
if (origin === "fragment") return ICON.fragment;
|
|
81
|
+
if (origin === "fn" || origin === "deco") return ICON.callable;
|
|
82
|
+
return ICON.variable;
|
|
83
|
+
}
|