@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,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
+ }
@@ -0,0 +1,4 @@
1
+ export { VennCompletionProvider } from "./completion.js";
2
+ export type { CompletionContext } from "./completion.types.js";
3
+ export { contextAt } from "./context.js";
4
+ export { modulePaths } from "./module-paths.js";