@telorun/ide-support 0.23.0 → 0.102.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 (42) hide show
  1. package/dist/completions/build.d.ts.map +1 -1
  2. package/dist/completions/build.js +7 -0
  3. package/dist/completions/detect-context.d.ts +33 -0
  4. package/dist/completions/detect-context.d.ts.map +1 -1
  5. package/dist/completions/detect-context.js +52 -0
  6. package/dist/completions/module-file-completions.d.ts +18 -0
  7. package/dist/completions/module-file-completions.d.ts.map +1 -0
  8. package/dist/completions/module-file-completions.js +59 -0
  9. package/dist/completions/resolve-node.d.ts +23 -5
  10. package/dist/completions/resolve-node.d.ts.map +1 -1
  11. package/dist/completions/resolve-node.js +54 -14
  12. package/dist/completions/value-tag-completions.d.ts +23 -0
  13. package/dist/completions/value-tag-completions.d.ts.map +1 -0
  14. package/dist/completions/value-tag-completions.js +39 -0
  15. package/dist/import-upgrades/build-import-upgrades.d.ts +3 -3
  16. package/dist/import-upgrades/build-import-upgrades.d.ts.map +1 -1
  17. package/dist/import-upgrades/index.d.ts +0 -1
  18. package/dist/import-upgrades/index.d.ts.map +1 -1
  19. package/dist/import-upgrades/index.js +0 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -0
  23. package/dist/types.d.ts +13 -1
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/value-tags/offered-value-tags.d.ts +56 -0
  26. package/dist/value-tags/offered-value-tags.d.ts.map +1 -0
  27. package/dist/value-tags/offered-value-tags.js +92 -0
  28. package/package.json +3 -2
  29. package/src/completions/build.ts +6 -0
  30. package/src/completions/detect-context.ts +84 -0
  31. package/src/completions/module-file-completions.ts +64 -0
  32. package/src/completions/resolve-node.ts +87 -19
  33. package/src/completions/value-tag-completions.ts +55 -0
  34. package/src/import-upgrades/build-import-upgrades.ts +3 -3
  35. package/src/import-upgrades/index.ts +0 -1
  36. package/src/index.ts +2 -0
  37. package/src/types.ts +22 -1
  38. package/src/value-tags/offered-value-tags.ts +127 -0
  39. package/dist/import-upgrades/parse-module-versions.d.ts +0 -19
  40. package/dist/import-upgrades/parse-module-versions.d.ts.map +0 -1
  41. package/dist/import-upgrades/parse-module-versions.js +0 -29
  42. package/src/import-upgrades/parse-module-versions.ts +0 -30
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A YAML tag an author may write on a value, and what it means to write one.
3
+ *
4
+ * The split with `@telorun/templating` is deliberate. An engine declares what a
5
+ * tag PRODUCES (`producedType()`) and where its CEL is (`expressionRegions`);
6
+ * this declares how the tag is PRESENTED to an author, which is editor knowledge
7
+ * no engine should carry. Applicability is derived from the engine's own
8
+ * declaration rather than from a list of names here — so the table says how to
9
+ * describe a tag, never which tags fit where.
10
+ *
11
+ * Shared by every editor host (VS Code completion, studio's source view and
12
+ * schema form), so the hosts cannot disagree about which tags a field takes.
13
+ */
14
+ export interface ValueTag {
15
+ /** Engine name, which is the YAML tag without its `!`. */
16
+ id: string;
17
+ /** How the tag is written. */
18
+ label: string;
19
+ /** One line on what the tag does. */
20
+ hint: string;
21
+ /** Only meaningful where the slot is EVALUATED — the tag decides what
22
+ * evaluation does with the value (`!cel` supplies the expression, `!literal`
23
+ * opts out of interpolation), so outside such a field it says nothing the
24
+ * plain value does not. An embed is the other case: it supplies a value, and
25
+ * evaluation was never involved. */
26
+ requiresEvalSlot?: boolean;
27
+ /** Set when the scalar under the tag is a module-root-relative location of
28
+ * something that ships with the module, saying what it may name. */
29
+ names?: "file" | "file-or-directory";
30
+ }
31
+ /** The authorable tag an engine name denotes, or undefined for a tag no host
32
+ * offers (`!ref`, `!sql`, an unknown one). */
33
+ export declare function valueTag(id: string): ValueTag | undefined;
34
+ /**
35
+ * The tags offerable at one field.
36
+ *
37
+ * Two rules, both read off the engine rather than off its name:
38
+ *
39
+ * - CAN its value satisfy the slot? A tag that declares a produced type is
40
+ * offered only where that type fits. This is what puts `!include-bytes` on a
41
+ * `Telo.Bytes` slot and keeps it off a string one — and what keeps
42
+ * `!literal`, which is always text, off a boolean predicate. Checked with
43
+ * the analyzer's own comparator so the editor and `telo check` agree about
44
+ * what fits. A tag declaring no produced type (`!cel`) produces whatever the
45
+ * slot says and constrains nothing here.
46
+ * - Is it MEANINGFUL here? A tag that decides what evaluation does with the
47
+ * value needs a slot that is evaluated at all: outside one, `!cel` is a
48
+ * value the runtime never evaluates (`CEL_IN_NON_EVAL_FIELD`), and
49
+ * `!literal` suppresses an interpolation that was never going to happen.
50
+ *
51
+ * `prop` undefined is a field with no declared schema, which constrains
52
+ * nothing. `evalMode` undefined means no rule decides whether the field is
53
+ * evaluated, so the second question is not asked.
54
+ */
55
+ export declare function offeredValueTags(prop: Record<string, unknown> | undefined, evalMode: "compile" | "runtime" | null | undefined): ValueTag[];
56
+ //# sourceMappingURL=offered-value-tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offered-value-tags.d.ts","sourceRoot":"","sources":["../../src/value-tags/offered-value-tags.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,QAAQ;IACvB,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb;;;;yCAIqC;IACrC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;yEACqE;IACrE,KAAK,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC;CACtC;AA4CD;+CAC+C;AAC/C,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAGzD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,GAAG,SAAS,GACjD,QAAQ,EAAE,CAWZ"}
@@ -0,0 +1,92 @@
1
+ import { checkSchemaCompatibility } from "@telorun/analyzer";
2
+ import { builtinEngines, producedTypeOf } from "@telorun/templating";
3
+ /**
4
+ * The tags an author may write, by engine name.
5
+ *
6
+ * `!ref` is absent on purpose: it names a RESOURCE rather than producing a
7
+ * value, so it belongs to a reference slot, never to a value one. `!sql` is
8
+ * absent until a host can edit it as SQL — a plain text box would be the wrong
9
+ * widget, and the hosts offer one set. An engine with no entry is simply not
10
+ * offered, which is the safe direction.
11
+ */
12
+ const AUTHORABLE = {
13
+ cel: {
14
+ label: "!cel",
15
+ hint: "A CEL expression, evaluated against this field's scope.",
16
+ requiresEvalSlot: true,
17
+ },
18
+ interpolate: {
19
+ label: "!interpolate",
20
+ hint: "Text with `${{ }}` holes, each a CEL expression; always a string.",
21
+ requiresEvalSlot: true,
22
+ },
23
+ literal: {
24
+ label: "!literal",
25
+ hint: "Opaque text. `${{ }}` inside it is not interpolated.",
26
+ requiresEvalSlot: true,
27
+ },
28
+ "include-text": {
29
+ label: "!include-text",
30
+ hint: "Contents of a file shipped with this module, as text.",
31
+ names: "file",
32
+ },
33
+ "include-bytes": {
34
+ label: "!include-bytes",
35
+ hint: "Contents of a file shipped with this module, as raw bytes.",
36
+ names: "file",
37
+ },
38
+ "module-path": {
39
+ label: "!module-path",
40
+ hint: "Location of a file or directory shipped with this module.",
41
+ names: "file-or-directory",
42
+ },
43
+ };
44
+ /** The authorable tag an engine name denotes, or undefined for a tag no host
45
+ * offers (`!ref`, `!sql`, an unknown one). */
46
+ export function valueTag(id) {
47
+ const entry = AUTHORABLE[id];
48
+ return entry ? { id, ...entry } : undefined;
49
+ }
50
+ /**
51
+ * The tags offerable at one field.
52
+ *
53
+ * Two rules, both read off the engine rather than off its name:
54
+ *
55
+ * - CAN its value satisfy the slot? A tag that declares a produced type is
56
+ * offered only where that type fits. This is what puts `!include-bytes` on a
57
+ * `Telo.Bytes` slot and keeps it off a string one — and what keeps
58
+ * `!literal`, which is always text, off a boolean predicate. Checked with
59
+ * the analyzer's own comparator so the editor and `telo check` agree about
60
+ * what fits. A tag declaring no produced type (`!cel`) produces whatever the
61
+ * slot says and constrains nothing here.
62
+ * - Is it MEANINGFUL here? A tag that decides what evaluation does with the
63
+ * value needs a slot that is evaluated at all: outside one, `!cel` is a
64
+ * value the runtime never evaluates (`CEL_IN_NON_EVAL_FIELD`), and
65
+ * `!literal` suppresses an interpolation that was never going to happen.
66
+ *
67
+ * `prop` undefined is a field with no declared schema, which constrains
68
+ * nothing. `evalMode` undefined means no rule decides whether the field is
69
+ * evaluated, so the second question is not asked.
70
+ */
71
+ export function offeredValueTags(prop, evalMode) {
72
+ const out = [];
73
+ for (const engine of builtinEngines) {
74
+ const tag = valueTag(engine.name);
75
+ if (!tag)
76
+ continue;
77
+ const produced = producedTypeOf(engine.name);
78
+ const fitsSlot = produced && prop ? producedFits(produced, prop) : true;
79
+ const meaningful = tag.requiresEvalSlot && evalMode !== undefined ? evalMode !== null : true;
80
+ if (fitsSlot && meaningful)
81
+ out.push(tag);
82
+ }
83
+ return out;
84
+ }
85
+ /** Whether a tag's produced type satisfies the slot's declared one. An
86
+ * undeclared slot accepts anything — it constrains nothing, so nothing about
87
+ * the value can contradict it. A union declares through its branches. */
88
+ function producedFits(produced, prop) {
89
+ if (!prop.type && !prop["x-telo-type"] && !prop.anyOf && !prop.oneOf)
90
+ return true;
91
+ return checkSchemaCompatibility(produced, prop).compatible;
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/ide-support",
3
- "version": "0.23.0",
3
+ "version": "0.102.0",
4
4
  "description": "Editor-host-agnostic IDE support (completions, diagnostic normalization) for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -37,7 +37,8 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "yaml": "^2.8.3",
40
- "@telorun/analyzer": "0.83.0"
40
+ "@telorun/analyzer": "0.102.0",
41
+ "@telorun/templating": "0.102.0"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@types/node": "^20.0.0",
@@ -12,6 +12,8 @@ import { celCompletions } from "./cel-completions.js";
12
12
  import { docIdentity } from "../doc-identity.js";
13
13
  import { detectContext, lookupRefConstraints, navigateSchema } from "./detect-context.js";
14
14
  import { importSourceCompletions } from "./import-source.js";
15
+ import { moduleFileCompletions } from "./module-file-completions.js";
16
+ import { valueTagCompletions } from "./value-tag-completions.js";
15
17
  import { propKeyCompletions } from "./prop-keys.js";
16
18
  import { CAPABILITY_VALUES } from "./valid-capabilities.js";
17
19
 
@@ -192,6 +194,10 @@ export async function buildCompletions(
192
194
  return kindCompletions(registry, ctx.docKind, ctx.yamlPath, ctx.replaceRange);
193
195
  }
194
196
  if (ctx.type === "capability") return capabilityCompletions();
197
+ if (ctx.type === "value-tag") return valueTagCompletions(ctx, registry);
198
+ if (ctx.type === "module-file") {
199
+ return moduleFileCompletions(ctx.names, ctx.prefix, ctx.replaceRange, adapter);
200
+ }
195
201
  if (ctx.type === "value-suggestions") {
196
202
  return valueSuggestions(registry, ctx.docKind, ctx.yamlPath, ctx.replaceRange);
197
203
  }
@@ -6,6 +6,7 @@ import {
6
6
  type CelSegment,
7
7
  } from "@telorun/analyzer";
8
8
  import type { ReplaceRange } from "../types.js";
9
+ import { valueTag } from "../value-tags/offered-value-tags.js";
9
10
  import { resolveNodeAtPosition } from "./resolve-node.js";
10
11
 
11
12
  export type { ReplaceRange };
@@ -88,6 +89,32 @@ export type CompletionCtx =
88
89
  segment: CelSegment;
89
90
  /** Cursor as a document offset. */
90
91
  offset: number;
92
+ }
93
+ | {
94
+ /** Cursor sits on a value's YAML tag (`root: !mo|`). What is offered is
95
+ * every tag the field takes, so its schema is addressed relative to the
96
+ * nearest enclosing resource. */
97
+ type: "value-tag";
98
+ /** Absent at a document declaring no resource kind. */
99
+ kind?: string;
100
+ yamlPath: string[];
101
+ /** Resource-relative, sequence indices kept — what the eval mode is
102
+ * resolved at. */
103
+ concretePath: string;
104
+ /** The value is a sequence item, so the field's schema is its item's. */
105
+ isItem: boolean;
106
+ /** Nothing follows the tag yet, so a pick may lead into the value. */
107
+ bare: boolean;
108
+ replaceRange: ReplaceRange;
109
+ }
110
+ | {
111
+ /** Cursor sits in the value of a tag naming a location inside the module
112
+ * (`!module-path ./pu|`). */
113
+ type: "module-file";
114
+ names: "file" | "file-or-directory";
115
+ /** Text from the start of the value to the cursor. */
116
+ prefix: string;
117
+ replaceRange: ReplaceRange;
91
118
  };
92
119
 
93
120
  /** Returns every schema branch reachable from `node` after peeling `anyOf` /
@@ -250,6 +277,38 @@ export function navigateSchema(
250
277
  return unionLeaves(current, leaves);
251
278
  }
252
279
 
280
+ /**
281
+ * The schema of the field at `path`, as DECLARED — combinators kept.
282
+ *
283
+ * `navigateSchema` merges a leaf's branches into one object node, which is what
284
+ * key completion wants and exactly what a type comparison must not see: a
285
+ * `Telo.HostPath` branch of an `anyOf` stops being one. `isItem` selects the
286
+ * field's item schema, for a value written as a sequence item.
287
+ */
288
+ export function fieldSchemaAt(
289
+ schema: Record<string, any>,
290
+ path: string[],
291
+ isItem: boolean,
292
+ schemaFrom?: SchemaFromResolver,
293
+ ): Record<string, any> | undefined {
294
+ if (path.length === 0) return undefined;
295
+ const parent = navigateSchema(schema, path.slice(0, -1), schemaFrom);
296
+ const key = path[path.length - 1]!;
297
+ let field = parent?.properties?.[key] ?? parent?.additionalProperties;
298
+ if (!field || typeof field !== "object") return undefined;
299
+ field = resolveLocalRef(field, schema);
300
+ if (isItem) {
301
+ while (field?.type === "array" && field.items) field = resolveLocalRef(field.items, schema);
302
+ }
303
+ return field;
304
+ }
305
+
306
+ /** `concretePath` relative to the resource map at `resourceConcrete`. */
307
+ function relativeConcretePath(concretePath: string, resourceConcrete: string): string {
308
+ if (!resourceConcrete) return concretePath;
309
+ return concretePath.slice(resourceConcrete.length).replace(/^\./, "");
310
+ }
311
+
253
312
  /** Merge multiple peeled schema branches into one node for completion purposes.
254
313
  * Property maps are unioned (first branch wins on key collision). `required`
255
314
  * becomes the intersection so optional-in-any-branch keys still surface.
@@ -328,6 +387,31 @@ export function detectContext(
328
387
  const { docKind } = resolved;
329
388
 
330
389
  if (resolved.slot === "value") {
390
+ const tag = resolved.tag;
391
+ if (tag?.prefix !== undefined) {
392
+ const depth = resolved.resourceDepth ?? 0;
393
+ return {
394
+ type: "value-tag",
395
+ kind: resolved.resourceKind ?? docKind,
396
+ yamlPath: resolved.path.slice(depth),
397
+ concretePath: relativeConcretePath(
398
+ resolved.concretePath ?? "",
399
+ resolved.resourceConcretePath ?? "",
400
+ ),
401
+ isItem: resolved.container === undefined,
402
+ bare: tag.bare,
403
+ replaceRange: tag.replaceRange,
404
+ };
405
+ }
406
+ const names = tag ? valueTag(tag.text.slice(1))?.names : undefined;
407
+ if (names && resolved.replaceRange) {
408
+ return {
409
+ type: "module-file",
410
+ names,
411
+ prefix: resolved.prefix ?? "",
412
+ replaceRange: resolved.replaceRange,
413
+ };
414
+ }
331
415
  // Inside a CEL body — structural completion does not apply; what completes
332
416
  // are the names the expression may use.
333
417
  if (resolved.cel) {
@@ -0,0 +1,64 @@
1
+ import type { CompletionResult, IdeEnvironmentAdapter, ReplaceRange } from "../types.js";
2
+
3
+ /** Caps one listing; a popover shows far fewer. */
4
+ const ENTRY_LIMIT = 200;
5
+
6
+ /**
7
+ * Completions for the value of a tag naming a location inside the module
8
+ * (`!include-text`, `!include-bytes`, `!module-path`).
9
+ *
10
+ * Paths are measured from the MODULE ROOT, never from the declaring file, so the
11
+ * listing is asked of the host against that root. The typed text up to its last
12
+ * `/` is the directory listed, and is kept on every insertion, so `./` stays
13
+ * where the author wrote it. A directory is a value in its own right where the
14
+ * tag may name one; where it names a file, a directory is only a way down, so a
15
+ * pick inserts its `/` and reopens completion. Dot-entries are listed only once
16
+ * the typed name starts with `.`, as a shell completes them.
17
+ *
18
+ * Nothing is offered above the root or for an absolute or URL-shaped path: the
19
+ * tag refuses those, and a listing there would offer what cannot be written.
20
+ */
21
+ export async function moduleFileCompletions(
22
+ names: "file" | "file-or-directory",
23
+ prefix: string,
24
+ replaceRange: ReplaceRange,
25
+ adapter: IdeEnvironmentAdapter | undefined,
26
+ ): Promise<CompletionResult[]> {
27
+ if (!adapter || /^["']/.test(prefix)) return [];
28
+ const lastSlash = prefix.lastIndexOf("/");
29
+ const dirPart = prefix.slice(0, lastSlash + 1);
30
+ const namePart = prefix.slice(lastSlash + 1);
31
+ if (!staysInModule(dirPart)) return [];
32
+
33
+ const entries = await adapter.listModuleEntries(dirPart === "" ? "." : dirPart);
34
+ return entries
35
+ .filter((e) => e.name.startsWith(namePart) && (namePart.startsWith(".") || !e.name.startsWith(".")))
36
+ .sort((a, b) => Number(b.directory) - Number(a.directory) || a.name.localeCompare(b.name))
37
+ .slice(0, ENTRY_LIMIT)
38
+ .map((entry) => {
39
+ const navigateOnly = entry.directory && names === "file";
40
+ const path = dirPart + entry.name + (navigateOnly ? "/" : "");
41
+ return {
42
+ label: entry.directory ? `${entry.name}/` : entry.name,
43
+ kind: entry.directory ? "folder" : "file",
44
+ insertText: path,
45
+ filterText: path,
46
+ replaceRange,
47
+ retrigger: navigateOnly,
48
+ sortText: `${entry.directory ? 0 : 1}_${entry.name}`,
49
+ } satisfies CompletionResult;
50
+ });
51
+ }
52
+
53
+ /** Whether a module-root-relative directory stays inside the module. Pure string
54
+ * work, by the rule the tags' own path grammar applies. */
55
+ function staysInModule(dir: string): boolean {
56
+ if (/^([/\\]|[a-z][a-z0-9+.-]*:)/i.test(dir)) return false;
57
+ let depth = 0;
58
+ for (const segment of dir.split(/[/\\]+/)) {
59
+ if (segment === "" || segment === ".") continue;
60
+ depth += segment === ".." ? -1 : 1;
61
+ if (depth < 0) return false;
62
+ }
63
+ return true;
64
+ }
@@ -41,13 +41,18 @@ export interface ResolvedCursor {
41
41
  /** Key slot: keys already present in the container (the key under the cursor
42
42
  * excluded so it still suggests itself). */
43
43
  existingKeys?: Set<string>;
44
- /** Key slot: the kind of the nearest enclosing inline resource (or the root
45
- * resource), whose schema the prop keys are completed against. Falls back to
46
- * the document kind at the root. */
44
+ /** The kind of the nearest enclosing inline resource (or the root resource),
45
+ * whose schema the slot is completed against. Absent at a root declaring no
46
+ * resource kind. */
47
47
  resourceKind?: string;
48
- /** Key slot: number of `path` segments that reach `resourceKind`'s map, so
49
- * the schema-relative path is `path.slice(resourceDepth)`. */
48
+ /** Number of `path` segments that reach `resourceKind`'s map, so the
49
+ * schema-relative path is `path.slice(resourceDepth)`. */
50
50
  resourceDepth?: number;
51
+ /** Value slot: the concrete path of `resourceKind`'s map, so the
52
+ * resource-relative concrete path is what follows it in `concretePath`. */
53
+ resourceConcretePath?: string;
54
+ /** Value slot: the scalar's tag, when written out in front of it. */
55
+ tag?: ResolvedTag;
51
56
  /** The path with sequence INDICES kept (`routes[0].handler.url`),
52
57
  * as distinct from `path`, through which arrays are transparent. This is the
53
58
  * address the analyzer speaks — an `x-telo-context` scope, an error-bearing
@@ -60,6 +65,17 @@ export interface ResolvedCursor {
60
65
  cel?: { segment: CelSegment; offset: number };
61
66
  }
62
67
 
68
+ export interface ResolvedTag {
69
+ /** The tag as written, `!` included. */
70
+ text: string;
71
+ replaceRange: { start: Position; end: Position };
72
+ /** Set when the cursor sits on the tag: its text up to the cursor. */
73
+ prefix?: string;
74
+ /** True when nothing follows the tag on its line — the value is still to be
75
+ * written. */
76
+ bare: boolean;
77
+ }
78
+
63
79
  function within(range: [number, number], offset: number): boolean {
64
80
  return offset >= range[0] && offset <= range[1];
65
81
  }
@@ -96,15 +112,43 @@ function resourceKindOf(map: AstMap): string | undefined {
96
112
 
97
113
  /** The kind + path-depth of the nearest enclosing inline resource (or the root
98
114
  * resource). `depth` is the number of `path` segments consumed to reach that
99
- * map, so a prop-key `yamlPath` relative to it is `path.slice(depth)`. */
115
+ * map, so a prop-key `yamlPath` relative to it is `path.slice(depth)`;
116
+ * `concrete` is that map's own concrete path. */
100
117
  interface ResourceScope {
101
118
  kind?: string;
102
119
  depth: number;
120
+ concrete: string;
103
121
  }
104
122
 
105
- function enter(scope: ResourceScope, map: AstMap, ancestorsLen: number): ResourceScope {
123
+ const ROOT_SCOPE: ResourceScope = { depth: 0, concrete: "" };
124
+
125
+ function enter(
126
+ scope: ResourceScope,
127
+ map: AstMap,
128
+ ancestorsLen: number,
129
+ concrete: string,
130
+ ): ResourceScope {
106
131
  const kind = resourceKindOf(map);
107
- return kind ? { kind, depth: ancestorsLen } : scope;
132
+ return kind ? { kind, depth: ancestorsLen, concrete } : scope;
133
+ }
134
+
135
+ /** The span of a scalar's tag, when it is written out in front of the value.
136
+ * The AST's range covers the value alone; the tag precedes it, separated by
137
+ * whitespace — or ends exactly at the range for a tag with no value yet. A tag
138
+ * whose text does not read back verbatim (`!!str`, which the parser expands)
139
+ * has no span. */
140
+ function tagSpan(node: AstNode, text: string): [number, number] | undefined {
141
+ if (node.kind !== "scalar" || !node.tag) return undefined;
142
+ let end = node.range[0];
143
+ while (end > 0 && /\s/.test(text[end - 1]!)) end--;
144
+ const start = end - node.tag.length;
145
+ return start >= 0 && text.slice(start, end) === node.tag ? [start, end] : undefined;
146
+ }
147
+
148
+ /** A value's span with its tag included, so a cursor on the tag is on the value. */
149
+ function valueSpan(node: AstNode, text: string): [number, number] {
150
+ const tag = tagSpan(node, text);
151
+ return tag ? [tag[0], node.range[1]] : node.range;
108
152
  }
109
153
 
110
154
  // ---------------------------------------------------------------------------
@@ -131,6 +175,7 @@ type Descent =
131
175
  keyName?: string;
132
176
  keyEnd: number;
133
177
  valueNode: AstNode;
178
+ scope: ResourceScope;
134
179
  }
135
180
  | { type: "empty" };
136
181
 
@@ -145,9 +190,10 @@ function descend(
145
190
  concrete: string,
146
191
  offset: number,
147
192
  scope: ResourceScope,
193
+ text: string,
148
194
  ): Descent | undefined {
149
195
  if (node.kind === "map") {
150
- const mapScope = enter(scope, node, ancestors.length);
196
+ const mapScope = enter(scope, node, ancestors.length, concrete);
151
197
  for (const pair of node.entries) {
152
198
  const keyName = scalarString(pair.key);
153
199
  if (within(pair.key.range, offset)) {
@@ -161,11 +207,15 @@ function descend(
161
207
  scope: mapScope,
162
208
  };
163
209
  }
164
- if (pair.value && within(pair.value.range, offset)) {
210
+ if (pair.value && within(valueSpan(pair.value, text), offset)) {
165
211
  const childAncestors = keyName != null ? [...ancestors, keyName] : ancestors;
166
212
  const childConcrete = keyName != null ? joinKey(concrete, keyName) : concrete;
167
213
  if (pair.value.kind === "map" || pair.value.kind === "seq") {
168
- return descend(pair.value, childAncestors, childConcrete, offset, mapScope) ?? { type: "empty" };
214
+ return (
215
+ descend(pair.value, childAncestors, childConcrete, offset, mapScope, text) ?? {
216
+ type: "empty",
217
+ }
218
+ );
169
219
  }
170
220
  return {
171
221
  type: "value",
@@ -175,6 +225,7 @@ function descend(
175
225
  keyName,
176
226
  keyEnd: pair.key.range[1],
177
227
  valueNode: pair.value,
228
+ scope: mapScope,
178
229
  };
179
230
  }
180
231
  }
@@ -186,10 +237,10 @@ function descend(
186
237
  // `x-telo-context` scope, an error-bearing region and a step's identity are
187
238
  // all addressed per item, so a CEL site is unreachable without the index.
188
239
  for (const [index, item] of node.items.entries()) {
189
- if (within(item.range, offset)) {
240
+ if (within(valueSpan(item, text), offset)) {
190
241
  const itemConcrete = `${concrete}[${index}]`;
191
242
  if (item.kind === "map" || item.kind === "seq") {
192
- return descend(item, ancestors, itemConcrete, offset, scope) ?? { type: "empty" };
243
+ return descend(item, ancestors, itemConcrete, offset, scope, text) ?? { type: "empty" };
193
244
  }
194
245
  // A bare scalar list item (`targets:\n - One`) has no enclosing map of
195
246
  // keyed siblings — leave `container` undefined rather than treating the
@@ -201,6 +252,7 @@ function descend(
201
252
  concretePath: itemConcrete,
202
253
  keyEnd: item.range[0],
203
254
  valueNode: item,
255
+ scope,
204
256
  };
205
257
  }
206
258
  }
@@ -243,7 +295,7 @@ function collectScopes(
243
295
  pairs: PairScope[],
244
296
  ): void {
245
297
  if (node.kind === "map") {
246
- const mapScope = enter(scope, node, ancestors.length);
298
+ const mapScope = enter(scope, node, ancestors.length, concrete);
247
299
  const keys = new Set<string>();
248
300
  let childColumn = -1;
249
301
  for (const pair of node.entries) {
@@ -309,7 +361,7 @@ function columnSearch(
309
361
  ): KeyResolution {
310
362
  const maps: MapScope[] = [];
311
363
  const pairs: PairScope[] = [];
312
- collectScopes(root, [], "", { depth: 0 }, lineOffsets, maps, pairs);
364
+ collectScopes(root, [], "", ROOT_SCOPE, lineOffsets, maps, pairs);
313
365
 
314
366
  // Sibling level: a map whose children already sit at the cursor's column.
315
367
  let sibling: MapScope | undefined;
@@ -349,7 +401,7 @@ function columnSearch(
349
401
  };
350
402
  }
351
403
 
352
- return { path: [], concrete: "", existingKeys: new Set(), scope: { depth: 0 } };
404
+ return { path: [], concrete: "", existingKeys: new Set(), scope: ROOT_SCOPE };
353
405
  }
354
406
 
355
407
  // ---------------------------------------------------------------------------
@@ -387,7 +439,7 @@ export function resolveNodeAtPosition(
387
439
  const doc = docs[docIndex];
388
440
  const docKind = docKindOf(doc);
389
441
 
390
- const found = doc.root ? descend(doc.root, [], "", offset, { depth: 0 }) : undefined;
442
+ const found = doc.root ? descend(doc.root, [], "", offset, ROOT_SCOPE, text) : undefined;
391
443
 
392
444
  // Cursor sits on an existing map key → key/prop-key position.
393
445
  if (found?.type === "key") {
@@ -443,6 +495,17 @@ export function resolveNodeAtPosition(
443
495
  }
444
496
 
445
497
  const clampedEnd = Math.min(offset, value.range[1]);
498
+ const span = tagSpan(value, text);
499
+ let tag: ResolvedTag | undefined;
500
+ if (span) {
501
+ const lineEnd = text.indexOf("\n", span[1]);
502
+ tag = {
503
+ text: value.tag!,
504
+ replaceRange: { start: toPos(span[0]), end: toPos(span[1]) },
505
+ prefix: within(span, offset) ? text.slice(span[0], offset) : undefined,
506
+ bare: text.slice(span[1], lineEnd < 0 ? text.length : lineEnd).trim() === "",
507
+ };
508
+ }
446
509
  return {
447
510
  docIndex,
448
511
  offset,
@@ -456,7 +519,12 @@ export function resolveNodeAtPosition(
456
519
  spaceAfterColon: value.range[0] - found.keyEnd >= 2,
457
520
  siblingKind: found.container ? siblingKindOf(found.container) : undefined,
458
521
  replaceRange: { start: toPos(value.range[0]), end: toPos(value.range[1]) },
459
- cel,
522
+ resourceKind: found.scope.kind,
523
+ resourceDepth: found.scope.depth,
524
+ resourceConcretePath: found.scope.concrete,
525
+ tag,
526
+ // The tag is not the value: a cursor on it is not inside its expression.
527
+ cel: tag?.prefix !== undefined ? undefined : cel,
460
528
  };
461
529
  }
462
530
 
@@ -464,7 +532,7 @@ export function resolveNodeAtPosition(
464
532
  // resolved by cursor column.
465
533
  const resolution: KeyResolution = doc.root
466
534
  ? columnSearch(doc.root, character, offset, lineOffsets)
467
- : { path: [], concrete: "", existingKeys: new Set<string>(), scope: { depth: 0 } };
535
+ : { path: [], concrete: "", existingKeys: new Set<string>(), scope: ROOT_SCOPE };
468
536
  return {
469
537
  docIndex,
470
538
  offset,
@@ -0,0 +1,55 @@
1
+ import type { AnalysisRegistry } from "@telorun/analyzer";
2
+ import type { CompletionResult, ReplaceRange } from "../types.js";
3
+ import { offeredValueTags, type ValueTag } from "../value-tags/offered-value-tags.js";
4
+ import { fieldSchemaAt, lookupRefConstraints } from "./detect-context.js";
5
+
6
+ /** `!ref` names a resource rather than producing a value, so it is offered at a
7
+ * reference slot alone — never beside the value tags. */
8
+ const REF_TAG: ValueTag = { id: "ref", label: "!ref", hint: "A reference to a named resource." };
9
+
10
+ export interface ValueTagSite {
11
+ kind?: string;
12
+ yamlPath: string[];
13
+ concretePath: string;
14
+ isItem: boolean;
15
+ bare: boolean;
16
+ replaceRange: ReplaceRange;
17
+ }
18
+
19
+ /**
20
+ * The YAML tags the field under the cursor takes.
21
+ *
22
+ * The same rule studio's schema form applies (`offeredValueTags`), asked of the
23
+ * field's declared schema and of the analyzer's eval mode at the site. Where the
24
+ * field cannot be resolved nothing is known against a tag, so every one is
25
+ * offered rather than none.
26
+ *
27
+ * On a tag with nothing after it, a pick carries a trailing space and, for a tag
28
+ * naming a module location, reopens completion on the path.
29
+ */
30
+ export function valueTagCompletions(
31
+ site: ValueTagSite,
32
+ registry: AnalysisRegistry | undefined,
33
+ ): CompletionResult[] {
34
+ return tagsAt(site, registry).map((tag, index) => ({
35
+ label: tag.label,
36
+ kind: "keyword",
37
+ detail: tag.hint,
38
+ insertText: site.bare ? `${tag.label} ` : tag.label,
39
+ replaceRange: site.replaceRange,
40
+ retrigger: site.bare && tag.names !== undefined,
41
+ sortText: String(index).padStart(2, "0"),
42
+ }));
43
+ }
44
+
45
+ function tagsAt(site: ValueTagSite, registry: AnalysisRegistry | undefined): ValueTag[] {
46
+ const { kind } = site;
47
+ const schema = kind ? registry?.resolveDefinition(kind)?.schema : undefined;
48
+ if (!registry || !kind || !schema) return [...offeredValueTags(undefined, undefined), REF_TAG];
49
+ const schemaFrom = (from: string) => registry.resolveSchemaFrom(from, kind);
50
+ if (lookupRefConstraints(schema as Record<string, any>, site.yamlPath, schemaFrom).length > 0) {
51
+ return [REF_TAG];
52
+ }
53
+ const field = fieldSchemaAt(schema as Record<string, any>, site.yamlPath, site.isItem, schemaFrom);
54
+ return offeredValueTags(field, registry.celEvalModeAt(kind, site.concretePath));
55
+ }