@telorun/ide-support 0.15.0 → 0.16.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 (69) hide show
  1. package/dist/cel/cursor-chain.d.ts +30 -0
  2. package/dist/cel/cursor-chain.d.ts.map +1 -0
  3. package/dist/cel/cursor-chain.js +30 -0
  4. package/dist/cel/symbols.d.ts +82 -0
  5. package/dist/cel/symbols.d.ts.map +1 -0
  6. package/dist/cel/symbols.js +147 -0
  7. package/dist/cel/tokens.d.ts +32 -0
  8. package/dist/cel/tokens.d.ts.map +1 -0
  9. package/dist/cel/tokens.js +162 -0
  10. package/dist/completions/build.d.ts +6 -2
  11. package/dist/completions/build.d.ts.map +1 -1
  12. package/dist/completions/build.js +52 -24
  13. package/dist/completions/call-inputs.d.ts +25 -0
  14. package/dist/completions/call-inputs.d.ts.map +1 -0
  15. package/dist/completions/call-inputs.js +78 -0
  16. package/dist/completions/cel-completions.d.ts +26 -0
  17. package/dist/completions/cel-completions.d.ts.map +1 -0
  18. package/dist/completions/cel-completions.js +78 -0
  19. package/dist/completions/detect-context.d.ts +47 -8
  20. package/dist/completions/detect-context.d.ts.map +1 -1
  21. package/dist/completions/detect-context.js +51 -15
  22. package/dist/completions/prop-keys.d.ts +5 -1
  23. package/dist/completions/prop-keys.d.ts.map +1 -1
  24. package/dist/completions/prop-keys.js +51 -3
  25. package/dist/completions/resolve-node.d.ts +9 -2
  26. package/dist/completions/resolve-node.d.ts.map +1 -1
  27. package/dist/completions/resolve-node.js +63 -21
  28. package/dist/definition/build-definition.d.ts +6 -2
  29. package/dist/definition/build-definition.d.ts.map +1 -1
  30. package/dist/definition/build-definition.js +16 -3
  31. package/dist/definition/locate-context-binding.d.ts +15 -0
  32. package/dist/definition/locate-context-binding.d.ts.map +1 -0
  33. package/dist/definition/locate-context-binding.js +35 -0
  34. package/dist/definition/locate-step.d.ts +13 -0
  35. package/dist/definition/locate-step.d.ts.map +1 -0
  36. package/dist/definition/locate-step.js +33 -0
  37. package/dist/definition/resolve-cel-target.d.ts +11 -1
  38. package/dist/definition/resolve-cel-target.d.ts.map +1 -1
  39. package/dist/definition/resolve-cel-target.js +14 -14
  40. package/dist/doc-identity.d.ts +17 -0
  41. package/dist/doc-identity.d.ts.map +1 -0
  42. package/dist/doc-identity.js +19 -0
  43. package/dist/hover/build-hover.d.ts +6 -2
  44. package/dist/hover/build-hover.d.ts.map +1 -1
  45. package/dist/hover/build-hover.js +64 -3
  46. package/dist/semantic-tokens/build-semantic-tokens.d.ts +13 -8
  47. package/dist/semantic-tokens/build-semantic-tokens.d.ts.map +1 -1
  48. package/dist/semantic-tokens/build-semantic-tokens.js +81 -37
  49. package/dist/types.d.ts +25 -5
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js +16 -2
  52. package/package.json +2 -2
  53. package/src/cel/cursor-chain.ts +58 -0
  54. package/src/cel/symbols.ts +189 -0
  55. package/src/cel/tokens.ts +169 -0
  56. package/src/completions/build.ts +85 -22
  57. package/src/completions/call-inputs.ts +92 -0
  58. package/src/completions/cel-completions.ts +108 -0
  59. package/src/completions/detect-context.ts +107 -13
  60. package/src/completions/prop-keys.ts +59 -2
  61. package/src/completions/resolve-node.ts +82 -17
  62. package/src/definition/build-definition.ts +30 -2
  63. package/src/definition/locate-context-binding.ts +53 -0
  64. package/src/definition/locate-step.ts +50 -0
  65. package/src/definition/resolve-cel-target.ts +25 -0
  66. package/src/doc-identity.ts +31 -0
  67. package/src/hover/build-hover.ts +67 -1
  68. package/src/semantic-tokens/build-semantic-tokens.ts +84 -30
  69. package/src/types.ts +47 -6
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The dotted chain the cursor is in the middle of typing.
3
+ *
4
+ * Deliberately TEXTUAL, and only for completion. `chainAt` (over the parsed
5
+ * CEL AST) is what hover and go-to-declaration use, because there the
6
+ * expression is complete and the cursor sits on a specific identifier whose
7
+ * span matters. Completion fires on text that frequently does not parse —
8
+ * `req.` is not an expression — so a parse-first approach would go silent
9
+ * exactly while the user is asking for help. An open segment recovers to its
10
+ * longest parseable PREFIX, which by definition drops the token being typed.
11
+ */
12
+ import type { CelSegment } from "@telorun/analyzer";
13
+ export interface CelCursorChain {
14
+ /** Identifiers resolved before the token under the cursor (`req.query.` → `["req","query"]`). */
15
+ prefix: string[];
16
+ /** The partial identifier being typed, possibly empty. */
17
+ token: string;
18
+ /** True when the cursor follows a `.` — a member position, where only the
19
+ * prefix's members are offered and functions are not. */
20
+ member: boolean;
21
+ }
22
+ /**
23
+ * Split the text before the cursor into a resolved prefix and a partial token.
24
+ *
25
+ * Returns undefined when the cursor does not follow an identifier-shaped run —
26
+ * after an operator, inside a string literal's content, at the start of an
27
+ * empty body — where the caller offers the root scope instead of a member list.
28
+ */
29
+ export declare function celCursorChain(text: string, segment: CelSegment, offset: number): CelCursorChain | undefined;
30
+ //# sourceMappingURL=cursor-chain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-chain.d.ts","sourceRoot":"","sources":["../../src/cel/cursor-chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B,iGAAiG;IACjG,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd;8DAC0D;IAC1D,MAAM,EAAE,OAAO,CAAC;CACjB;AAaD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,MAAM,GACb,cAAc,GAAG,SAAS,CAY5B"}
@@ -0,0 +1,30 @@
1
+ /** Where `segment.source` starts in document offsets. The segment range spans
2
+ * the delimiters (`${{ … }}`) and any trimmed whitespace, so the body has to be
3
+ * located inside it rather than assumed to start at `range[0]`. */
4
+ function bodyStart(text, segment) {
5
+ const span = text.slice(segment.range[0], segment.range[1]);
6
+ const at = span.indexOf(segment.source);
7
+ return at < 0 ? segment.range[0] : segment.range[0] + at;
8
+ }
9
+ const TRAILING_CHAIN = /[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*\.?$/;
10
+ /**
11
+ * Split the text before the cursor into a resolved prefix and a partial token.
12
+ *
13
+ * Returns undefined when the cursor does not follow an identifier-shaped run —
14
+ * after an operator, inside a string literal's content, at the start of an
15
+ * empty body — where the caller offers the root scope instead of a member list.
16
+ */
17
+ export function celCursorChain(text, segment, offset) {
18
+ const start = bodyStart(text, segment);
19
+ const index = Math.max(0, Math.min(offset - start, segment.source.length));
20
+ const before = segment.source.slice(0, index);
21
+ const match = TRAILING_CHAIN.exec(before);
22
+ if (!match)
23
+ return undefined;
24
+ const run = match[0];
25
+ if (run.endsWith(".")) {
26
+ return { prefix: run.slice(0, -1).split("."), token: "", member: true };
27
+ }
28
+ const parts = run.split(".");
29
+ return { prefix: parts.slice(0, -1), token: parts[parts.length - 1], member: parts.length > 1 };
30
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * **What a name means inside a CEL expression.**
3
+ *
4
+ * The TYPE half of CEL language support — completion's candidate list and
5
+ * hover's tooltip are both this, read off the scope the analyzer resolved. It
6
+ * is deliberately separate from the DECLARATION half (`definition/`), which
7
+ * answers where a name was written: the two take different inputs and
8
+ * legitimately disagree. `steps.encode.result` has a type and no manifest node
9
+ * to jump to; a transport binding like `request` has a scope entry and no
10
+ * declaration at all. Joining them is hover's job, not this module's.
11
+ */
12
+ import type { CelScope } from "@telorun/analyzer";
13
+ /** One name in scope, with whatever the scope knows about it. Both `type` and
14
+ * `schema` are optional and neither implies the other: a CEL environment
15
+ * variable carries a type and no schema, a context property carries a schema
16
+ * and gets its type from it. */
17
+ export interface CelSymbol {
18
+ name: string;
19
+ /** CEL type name (`int`, `string`, `map`), when the environment declares one. */
20
+ type?: string;
21
+ /** JSON Schema node, when the context declares the shape. */
22
+ schema?: Record<string, any>;
23
+ description?: string;
24
+ }
25
+ /**
26
+ * One callable, with every overload the environment registered for it.
27
+ *
28
+ * Grouped rather than one entry per overload: the registry declares a signature
29
+ * per accepted argument list — `double` has four — and offering each as its own
30
+ * candidate turns a completion list into four identical labels the author
31
+ * cannot choose between. What varies between them is the signature, so that is
32
+ * what the grouped entry carries.
33
+ */
34
+ export interface CelFunctionSymbol {
35
+ name: string;
36
+ /** Every registered overload's signature, in registration order. */
37
+ signatures: string[];
38
+ /** The type a receiver-style call is made ON (`string.startsWith`), or null
39
+ * for a global function. Part of the grouping key, since a global and a
40
+ * method sharing a name are genuinely two callables. */
41
+ receiverType: string | null;
42
+ /** The first description any overload carries — they describe the function,
43
+ * not the individual argument list. */
44
+ description?: string;
45
+ }
46
+ /** The type a schema node declares, rendered for display. Unions are joined
47
+ * rather than collapsed — a slot admitting several shapes says so. */
48
+ export declare function schemaTypeName(schema: Record<string, any> | undefined): string | undefined;
49
+ /**
50
+ * The names an expression may start with.
51
+ *
52
+ * Both sources, because neither is complete: the context schema carries the
53
+ * scope's own bindings (`steps`, `error`, a kind's named bindings, a transport's
54
+ * `request`), while the environment carries what was registered onto it
55
+ * directly — which is where the kernel globals live when no context applied.
56
+ * A name in both takes its schema from the context, which is the narrower.
57
+ */
58
+ export declare function celRootSymbols(scope: CelScope): CelSymbol[];
59
+ /**
60
+ * The members available after `prefix`.
61
+ *
62
+ * Empty when the prefix resolves to nothing OR to something whose shape the
63
+ * scope does not declare — an open node, a live value, a permissive contract.
64
+ * That is the honest answer: offering a guess here would be offering names the
65
+ * checker has no opinion about, which is exactly what the shared scope rule
66
+ * exists to prevent.
67
+ */
68
+ export declare function celMemberSymbols(scope: CelScope, prefix: string[]): CelSymbol[];
69
+ /** What the chain `parts` resolves to — used for hover, where the cursor sits on
70
+ * one identifier of a complete chain and the symbol wanted is the one at that
71
+ * identifier, not at the chain's tail. */
72
+ export declare function celSymbolAt(scope: CelScope, parts: string[]): CelSymbol | undefined;
73
+ /**
74
+ * The callables the environment declares, one entry per function.
75
+ *
76
+ * Read off `getDefinitions()` rather than a curated list, exactly as the call
77
+ * classifier does — so a function the registry gained is offered without this
78
+ * module being told, and one it never had is never offered. Overloads are
79
+ * folded into their function; see {@link CelFunctionSymbol}.
80
+ */
81
+ export declare function celFunctions(scope: CelScope): CelFunctionSymbol[];
82
+ //# sourceMappingURL=symbols.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symbols.d.ts","sourceRoot":"","sources":["../../src/cel/symbols.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGlD;;;iCAGiC;AACjC,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB;;6DAEyD;IACzD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;4CACwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;uEACuE;AACvE,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAgB1F;AASD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,EAAE,CAoB3D;AAcD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAc/E;AAED;;2CAE2C;AAC3C,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,SAAS,CAanF;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,iBAAiB,EAAE,CAkBjE"}
@@ -0,0 +1,147 @@
1
+ import { navigateSchema } from "../completions/detect-context.js";
2
+ /** The type a schema node declares, rendered for display. Unions are joined
3
+ * rather than collapsed — a slot admitting several shapes says so. */
4
+ export function schemaTypeName(schema) {
5
+ if (!schema)
6
+ return undefined;
7
+ const valueType = schema["x-telo-type"];
8
+ if (typeof valueType === "string")
9
+ return valueType;
10
+ if (valueType && typeof valueType === "object" && typeof valueType.name === "string") {
11
+ return valueType.name;
12
+ }
13
+ const t = schema.type;
14
+ if (Array.isArray(t))
15
+ return t.join(" | ");
16
+ if (typeof t === "string")
17
+ return t;
18
+ if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {
19
+ const branches = (schema.anyOf ?? schema.oneOf);
20
+ const names = branches.map((b) => schemaTypeName(b)).filter(Boolean);
21
+ if (names.length > 0)
22
+ return [...new Set(names)].join(" | ");
23
+ }
24
+ return undefined;
25
+ }
26
+ /** The context's property map, or an empty one when the site is typed by the
27
+ * environment alone. */
28
+ function contextProperties(scope) {
29
+ const props = scope.contextSchema?.properties;
30
+ return props && typeof props === "object" ? props : {};
31
+ }
32
+ /**
33
+ * The names an expression may start with.
34
+ *
35
+ * Both sources, because neither is complete: the context schema carries the
36
+ * scope's own bindings (`steps`, `error`, a kind's named bindings, a transport's
37
+ * `request`), while the environment carries what was registered onto it
38
+ * directly — which is where the kernel globals live when no context applied.
39
+ * A name in both takes its schema from the context, which is the narrower.
40
+ */
41
+ export function celRootSymbols(scope) {
42
+ const out = new Map();
43
+ for (const variable of scope.env.getDefinitions().variables) {
44
+ out.set(variable.name, {
45
+ name: variable.name,
46
+ type: variable.type,
47
+ description: variable.description ?? undefined,
48
+ });
49
+ }
50
+ for (const [name, schema] of Object.entries(contextProperties(scope))) {
51
+ const node = schema;
52
+ out.set(name, {
53
+ name,
54
+ type: schemaTypeName(node) ?? out.get(name)?.type,
55
+ schema: node,
56
+ description: typeof node.description === "string" ? node.description : out.get(name)?.description,
57
+ });
58
+ }
59
+ return [...out.values()];
60
+ }
61
+ /** The schema at a dotted path from the scope root, or undefined when the path
62
+ * leaves what the context declares. Navigation is the shared schema walk, so a
63
+ * member reached through an array, a `$ref` or an `anyOf` branch resolves the
64
+ * same way it does for a structural field. */
65
+ function schemaAtPath(scope, parts) {
66
+ if (parts.length === 0)
67
+ return scope.contextSchema ?? undefined;
68
+ const root = contextProperties(scope)[parts[0]];
69
+ if (!root)
70
+ return undefined;
71
+ if (parts.length === 1)
72
+ return root;
73
+ return navigateSchema(root, parts.slice(1));
74
+ }
75
+ /**
76
+ * The members available after `prefix`.
77
+ *
78
+ * Empty when the prefix resolves to nothing OR to something whose shape the
79
+ * scope does not declare — an open node, a live value, a permissive contract.
80
+ * That is the honest answer: offering a guess here would be offering names the
81
+ * checker has no opinion about, which is exactly what the shared scope rule
82
+ * exists to prevent.
83
+ */
84
+ export function celMemberSymbols(scope, prefix) {
85
+ if (prefix.length === 0)
86
+ return celRootSymbols(scope);
87
+ const node = schemaAtPath(scope, prefix);
88
+ const props = node?.properties;
89
+ if (!props || typeof props !== "object")
90
+ return [];
91
+ return Object.entries(props).map(([name, raw]) => {
92
+ const child = raw;
93
+ return {
94
+ name,
95
+ type: schemaTypeName(child),
96
+ schema: child,
97
+ description: typeof child.description === "string" ? child.description : undefined,
98
+ };
99
+ });
100
+ }
101
+ /** What the chain `parts` resolves to — used for hover, where the cursor sits on
102
+ * one identifier of a complete chain and the symbol wanted is the one at that
103
+ * identifier, not at the chain's tail. */
104
+ export function celSymbolAt(scope, parts) {
105
+ if (parts.length === 0)
106
+ return undefined;
107
+ if (parts.length === 1) {
108
+ return celRootSymbols(scope).find((s) => s.name === parts[0]);
109
+ }
110
+ const schema = schemaAtPath(scope, parts);
111
+ if (!schema)
112
+ return undefined;
113
+ return {
114
+ name: parts[parts.length - 1],
115
+ type: schemaTypeName(schema),
116
+ schema,
117
+ description: typeof schema.description === "string" ? schema.description : undefined,
118
+ };
119
+ }
120
+ /**
121
+ * The callables the environment declares, one entry per function.
122
+ *
123
+ * Read off `getDefinitions()` rather than a curated list, exactly as the call
124
+ * classifier does — so a function the registry gained is offered without this
125
+ * module being told, and one it never had is never offered. Overloads are
126
+ * folded into their function; see {@link CelFunctionSymbol}.
127
+ */
128
+ export function celFunctions(scope) {
129
+ const byKey = new Map();
130
+ for (const fn of scope.env.getDefinitions().functions) {
131
+ const key = `${fn.receiverType ?? ""}.${fn.name}`;
132
+ const existing = byKey.get(key);
133
+ if (existing) {
134
+ if (!existing.signatures.includes(fn.signature))
135
+ existing.signatures.push(fn.signature);
136
+ existing.description ??= fn.description ?? undefined;
137
+ continue;
138
+ }
139
+ byKey.set(key, {
140
+ name: fn.name,
141
+ signatures: [fn.signature],
142
+ receiverType: fn.receiverType,
143
+ description: fn.description ?? undefined,
144
+ });
145
+ }
146
+ return [...byKey.values()];
147
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Semantic tokens for the inside of a CEL body.
3
+ *
4
+ * A `!cel "..."` scalar is not a string, and under a stock YAML grammar it is
5
+ * painted as one. Colouring it through the SEMANTIC layer rather than a grammar
6
+ * is what makes one implementation serve both hosts — VS Code and the editor's
7
+ * Monaco already share `buildSemanticTokens`, while a Monarch tokenizer beside
8
+ * the TextMate one would be a second CEL lexer to keep in agreement.
9
+ *
10
+ * It is also the only layer that can be right about NAMES. A grammar knows the
11
+ * seven kernel roots someone hardcoded into it, which is why `request` and
12
+ * `steps` go uncoloured today; the scope query knows what is actually in scope
13
+ * at this exact site. So a name the scope confirms is coloured and a name it
14
+ * cannot is left alone — the quiet signal an unresolved `kind:` already gives.
15
+ */
16
+ import { type CelScope, type CelSegment } from "@telorun/analyzer";
17
+ import type { SemanticTokenType } from "../types.js";
18
+ /** A token before it is placed on a line — document offsets, resolved by the
19
+ * caller which owns the line table. */
20
+ export interface CelTokenSpan {
21
+ range: [number, number];
22
+ type: SemanticTokenType;
23
+ }
24
+ /**
25
+ * Tokens for one CEL segment.
26
+ *
27
+ * A body that does not parse yields nothing: mid-typing is the normal case
28
+ * here, and the analyzer reports the syntax error itself. Only that failure is
29
+ * tolerated — a defect in the CEL wrapper propagates.
30
+ */
31
+ export declare function celSegmentTokens(text: string, segment: CelSegment, scope: CelScope | undefined): CelTokenSpan[];
32
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/cel/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAA+B,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAChG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIrD;wCACwC;AACxC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,IAAI,EAAE,iBAAiB,CAAC;CACzB;AAeD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,UAAU,EACnB,KAAK,EAAE,QAAQ,GAAG,SAAS,GAC1B,YAAY,EAAE,CAqHhB"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Semantic tokens for the inside of a CEL body.
3
+ *
4
+ * A `!cel "..."` scalar is not a string, and under a stock YAML grammar it is
5
+ * painted as one. Colouring it through the SEMANTIC layer rather than a grammar
6
+ * is what makes one implementation serve both hosts — VS Code and the editor's
7
+ * Monaco already share `buildSemanticTokens`, while a Monarch tokenizer beside
8
+ * the TextMate one would be a second CEL lexer to keep in agreement.
9
+ *
10
+ * It is also the only layer that can be right about NAMES. A grammar knows the
11
+ * seven kernel roots someone hardcoded into it, which is why `request` and
12
+ * `steps` go uncoloured today; the scope query knows what is actually in scope
13
+ * at this exact site. So a name the scope confirms is coloured and a name it
14
+ * cannot is left alone — the quiet signal an unresolved `kind:` already gives.
15
+ */
16
+ import { CelParseError } from "@telorun/analyzer";
17
+ import { flattenChain } from "../cel-chain.js";
18
+ import { celRootSymbols, celSymbolAt } from "./symbols.js";
19
+ const LITERAL_TYPE = (value) => {
20
+ if (typeof value === "number" || typeof value === "bigint")
21
+ return "number";
22
+ if (typeof value === "string")
23
+ return "string";
24
+ if (typeof value === "boolean" || value === null)
25
+ return "keyword";
26
+ return undefined;
27
+ };
28
+ /**
29
+ * Tokens for one CEL segment.
30
+ *
31
+ * A body that does not parse yields nothing: mid-typing is the normal case
32
+ * here, and the analyzer reports the syntax error itself. Only that failure is
33
+ * tolerated — a defect in the CEL wrapper propagates.
34
+ */
35
+ export function celSegmentTokens(text, segment, scope) {
36
+ let ast;
37
+ try {
38
+ ast = segment.ast();
39
+ }
40
+ catch (error) {
41
+ if (!(error instanceof CelParseError))
42
+ throw error;
43
+ return [];
44
+ }
45
+ const out = [];
46
+ const mode = scope ? "scoped" : "syntactic";
47
+ // Resolved once per SEGMENT, not once per name: the root set is a property of
48
+ // the scope, and this runs over the whole document on every token request.
49
+ const inScope = scope ? new Set(celRootSymbols(scope).map((s) => s.name)) : undefined;
50
+ /** True when the scope confirms the chain `parts`. Callers walk a chain
51
+ * left-to-right and stop at the first unconfirmed hop, so each name is
52
+ * resolved once rather than the chain being re-walked from its root per hop. */
53
+ const confirms = (parts) => {
54
+ if (mode === "syntactic")
55
+ return true;
56
+ if (parts.length === 1)
57
+ return inScope.has(parts[0]);
58
+ return celSymbolAt(scope, parts) !== undefined;
59
+ };
60
+ /** The span of `name` between two offsets, or undefined when it is not there
61
+ * (a receiver-style call written across an unexpected layout). Nothing is
62
+ * invented: a token placed on a guessed span would paint the wrong text. */
63
+ const spanOf = (name, from, to) => {
64
+ const at = text.indexOf(name, from);
65
+ return at >= 0 && at + name.length <= to ? [at, at + name.length] : undefined;
66
+ };
67
+ const visit = (node) => {
68
+ switch (node.kind) {
69
+ case "literal": {
70
+ const type = LITERAL_TYPE(node.value);
71
+ if (type)
72
+ out.push({ range: node.range, type });
73
+ return;
74
+ }
75
+ case "ident":
76
+ // A root-position name is a scope the runtime injects, not data the
77
+ // author declared — see `SemanticTokenType`.
78
+ if (confirms([node.name]))
79
+ out.push({ range: node.range, type: "namespace" });
80
+ return;
81
+ case "member": {
82
+ // A plain chain is resolved as a whole, so each hop is judged against
83
+ // what the one before it declared — `resources.db.url` colours `db` only
84
+ // if `resources` really carries it. A chain rooted in something computed
85
+ // (`f().x`) flattens to nothing, and the scope has no opinion about it.
86
+ const parts = flattenChain(node);
87
+ if (parts) {
88
+ // A hop the scope cannot confirm makes every hop past it unresolvable
89
+ // too, so the walk STOPS there rather than re-resolving the rest — the
90
+ // chain is resolved once, not once per hop.
91
+ for (let i = 0; i < parts.length; i++) {
92
+ if (!confirms(parts.slice(0, i + 1).map((p) => p.name)))
93
+ break;
94
+ out.push({ range: parts[i].range, type: i === 0 ? "namespace" : "property" });
95
+ }
96
+ return;
97
+ }
98
+ visit(node.target);
99
+ if (mode === "syntactic")
100
+ out.push({ range: node.propertyRange, type: "property" });
101
+ return;
102
+ }
103
+ case "index":
104
+ visit(node.target);
105
+ visit(node.index);
106
+ return;
107
+ case "call": {
108
+ // `foo(...)` starts with its own name, so the head of the node IS the
109
+ // callee's span.
110
+ out.push({ range: [node.range[0], node.range[0] + node.name.length], type: "function" });
111
+ for (const arg of node.args)
112
+ visit(arg);
113
+ return;
114
+ }
115
+ case "methodCall": {
116
+ visit(node.receiver);
117
+ const span = spanOf(node.name, node.receiver.range[1], node.range[1]);
118
+ if (span)
119
+ out.push({ range: span, type: "function" });
120
+ for (const arg of node.args)
121
+ visit(arg);
122
+ return;
123
+ }
124
+ case "list":
125
+ for (const item of node.items)
126
+ visit(item);
127
+ return;
128
+ case "map":
129
+ for (const entry of node.entries) {
130
+ visit(entry.key);
131
+ visit(entry.value);
132
+ }
133
+ return;
134
+ case "ternary":
135
+ visit(node.cond);
136
+ visit(node.then);
137
+ visit(node.else);
138
+ return;
139
+ case "unary": {
140
+ const span = spanOf(node.op, node.range[0], node.operand.range[0]);
141
+ if (span)
142
+ out.push({ range: span, type: "operator" });
143
+ visit(node.operand);
144
+ return;
145
+ }
146
+ case "binary": {
147
+ visit(node.left);
148
+ const span = spanOf(node.op, node.left.range[1], node.right.range[0]);
149
+ if (span)
150
+ out.push({ range: span, type: "operator" });
151
+ visit(node.right);
152
+ return;
153
+ }
154
+ }
155
+ // Exhaustive by construction: a new `CelNode` variant fails the build here
156
+ // rather than going silently uncoloured.
157
+ const unhandled = node;
158
+ throw new Error(`Unhandled CEL node: ${JSON.stringify(unhandled)}`);
159
+ };
160
+ visit(ast);
161
+ return out;
162
+ }
@@ -1,4 +1,8 @@
1
- import { type AnalysisRegistry, type AstDocument } from "@telorun/analyzer";
1
+ import { type AnalysisRegistry, type AstDocument, type ManifestAnalysis } from "@telorun/analyzer";
2
2
  import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
3
- export declare function buildCompletions(text: string, line: number, character: number, registry: AnalysisRegistry | undefined, adapter?: IdeEnvironmentAdapter, docs?: AstDocument[]): Promise<CompletionResult[]>;
3
+ export declare function buildCompletions(text: string, line: number, character: number, registry: AnalysisRegistry | undefined, adapter?: IdeEnvironmentAdapter, docs?: AstDocument[],
4
+ /** The host's analysis of the manifests it loaded. Required for anything
5
+ * that has to resolve against the manifest SET — CEL completion, and a
6
+ * target's declared inputs. */
7
+ analysis?: ManifestAnalysis): Promise<CompletionResult[]>;
4
8
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/completions/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAe,MAAM,mBAAmB,CAAC;AACrG,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AA8I3E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,OAAO,CAAC,EAAE,qBAAqB,EAC/B,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyB7B"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/completions/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAEhB,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAqK3E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,OAAO,CAAC,EAAE,qBAAqB,EAC/B,IAAI,CAAC,EAAE,WAAW,EAAE;AACpB;;gCAEgC;AAChC,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuD7B"}
@@ -1,5 +1,8 @@
1
- import { parseToAst } from "@telorun/analyzer";
2
- import { detectContext, lookupRefConstraints } from "./detect-context.js";
1
+ import { parseToAst, } from "@telorun/analyzer";
2
+ import { callInputsAt } from "./call-inputs.js";
3
+ import { celCompletions } from "./cel-completions.js";
4
+ import { docIdentity } from "../doc-identity.js";
5
+ import { detectContext, lookupRefConstraints, navigateSchema } from "./detect-context.js";
3
6
  import { importSourceCompletions } from "./import-source.js";
4
7
  import { propKeyCompletions } from "./prop-keys.js";
5
8
  import { CAPABILITY_VALUES } from "./valid-capabilities.js";
@@ -8,24 +11,10 @@ import { CAPABILITY_VALUES } from "./valid-capabilities.js";
8
11
  * either is simply skipped; the analyzer remains the source of truth. */
9
12
  function extractInFileResources(docs) {
10
13
  const out = [];
11
- const scalar = (node) => node?.kind === "scalar" && typeof node.value === "string" ? node.value : undefined;
12
14
  for (const doc of docs) {
13
- if (doc.root?.kind !== "map")
14
- continue;
15
- let kind;
16
- let name;
17
- for (const pair of doc.root.entries) {
18
- const key = scalar(pair.key);
19
- if (key === "kind")
20
- kind = scalar(pair.value);
21
- else if (key === "metadata" && pair.value?.kind === "map") {
22
- const meta = pair.value;
23
- const nameEntry = meta.entries.find((e) => scalar(e.key) === "name");
24
- name = scalar(nameEntry?.value);
25
- }
26
- }
27
- if (kind && name)
28
- out.push({ kind, name });
15
+ const identity = docIdentity(doc);
16
+ if (identity.kind && identity.name)
17
+ out.push({ kind: identity.kind, name: identity.name });
29
18
  }
30
19
  return out;
31
20
  }
@@ -78,7 +67,7 @@ function refConstrainedKinds(registry, parentDocKind, parentYamlPath) {
78
67
  const definition = registry.resolveDefinition(parentDocKind);
79
68
  if (!definition?.schema)
80
69
  return undefined;
81
- const constraints = lookupRefConstraints(definition.schema, parentYamlPath);
70
+ const constraints = lookupRefConstraints(definition.schema, parentYamlPath, (from) => registry.resolveSchemaFrom(from, parentDocKind));
82
71
  if (constraints.length === 0)
83
72
  return undefined;
84
73
  const resolved = constraints.map((c) => registry.userFacingKindsForRef(c));
@@ -111,6 +100,33 @@ function kindCompletions(registry, docKind, yamlPath, replaceRange) {
111
100
  }
112
101
  return results;
113
102
  }
103
+ /**
104
+ * The values a field's schema says it may take.
105
+ *
106
+ * `enum` is closed and `examples` open — the same distinction `propertyNames`
107
+ * carries for a map's keys, one level down. Nothing is offered when the schema
108
+ * declares neither, which is most slots.
109
+ */
110
+ function valueSuggestions(registry, docKind, yamlPath, replaceRange) {
111
+ const definition = registry?.resolveDefinition(docKind);
112
+ if (!registry || !definition?.schema || yamlPath.length === 0)
113
+ return [];
114
+ const field = navigateSchema(definition.schema, yamlPath, (from) => registry.resolveSchemaFrom(from, docKind));
115
+ if (!field)
116
+ return [];
117
+ const closed = Array.isArray(field.enum) ? field.enum : undefined;
118
+ const values = closed ?? (Array.isArray(field.examples) ? field.examples : []);
119
+ return values
120
+ .filter((v) => v !== null && typeof v !== "object")
121
+ .map((value) => ({
122
+ label: String(value),
123
+ kind: "enumMember",
124
+ detail: closed ? "allowed value" : "known value",
125
+ // Whole-value replacement, so picking over a partially typed value leaves
126
+ // no suffix — the rule every other value completion here follows.
127
+ replaceRange,
128
+ }));
129
+ }
114
130
  function capabilityCompletions() {
115
131
  return CAPABILITY_VALUES.map((cap) => ({
116
132
  label: cap,
@@ -118,7 +134,11 @@ function capabilityCompletions() {
118
134
  detail: "Telo capability",
119
135
  }));
120
136
  }
121
- export async function buildCompletions(text, line, character, registry, adapter, docs) {
137
+ export async function buildCompletions(text, line, character, registry, adapter, docs,
138
+ /** The host's analysis of the manifests it loaded. Required for anything
139
+ * that has to resolve against the manifest SET — CEL completion, and a
140
+ * target's declared inputs. */
141
+ analysis) {
122
142
  // Reuse the host's already-parsed AST when it matches the current buffer;
123
143
  // otherwise parse once here (Part 1 stands alone). Both `detectContext` and
124
144
  // ref-name in-file resource extraction share this single parse.
@@ -131,10 +151,16 @@ export async function buildCompletions(text, line, character, registry, adapter,
131
151
  }
132
152
  if (ctx.type === "capability")
133
153
  return capabilityCompletions();
154
+ if (ctx.type === "value-suggestions") {
155
+ return valueSuggestions(registry, ctx.docKind, ctx.yamlPath, ctx.replaceRange);
156
+ }
157
+ if (ctx.type === "cel") {
158
+ return celCompletions(text, ctx.segment, ctx.offset, ctx.concretePath, docIdentity(astDocs[ctx.docIndex]), analysis?.celScope);
159
+ }
134
160
  if (ctx.type === "ref-name") {
135
161
  const definition = registry?.resolveDefinition(ctx.docKind);
136
- const refConstraints = definition?.schema
137
- ? lookupRefConstraints(definition.schema, ctx.yamlPath)
162
+ const refConstraints = registry && definition?.schema
163
+ ? lookupRefConstraints(definition.schema, ctx.yamlPath, (from) => registry.resolveSchemaFrom(from, ctx.docKind))
138
164
  : [];
139
165
  return refNameCompletions(astDocs, ctx.refKind, refConstraints, registry, ctx.replaceRange);
140
166
  }
@@ -144,5 +170,7 @@ export async function buildCompletions(text, line, character, registry, adapter,
144
170
  }
145
171
  return [];
146
172
  }
147
- return propKeyCompletions(ctx.docKind, ctx.yamlPath, ctx.existingKeys, registry);
173
+ // A slot that IS an enclosing call's argument map completes from the target's
174
+ // declared inputs rather than from its own (open) schema.
175
+ return propKeyCompletions(ctx.docKind, ctx.yamlPath, ctx.existingKeys, registry, callInputsAt(registry, analysis, docIdentity(astDocs[ctx.docIndex]).kind ?? ctx.docKind, docIdentity(astDocs[ctx.docIndex]).name, ctx.concretePath));
148
176
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The arguments a call site declares.
3
+ *
4
+ * A slot that transfers control names its argument slot on its own `x-telo-ref`
5
+ * — `inputs: /inputs`, a JSON Pointer relative to the object enclosing the slot.
6
+ * That is the only thing tying an `inputs:` map to the resource it is arguments
7
+ * FOR: the map itself is an open object, and the reference sits in a sibling
8
+ * field whose name no walker may assume.
9
+ *
10
+ * Reading the pointer here means completion offers exactly the keys the invoked
11
+ * target declares — resolved through the shared contract resolver, so they are
12
+ * the keys `telo check` validates that call against and the kernel binds at
13
+ * dispatch, instance declaration first.
14
+ */
15
+ import { type AnalysisRegistry, type ManifestAnalysis } from "@telorun/analyzer";
16
+ /**
17
+ * The declared input contract of the call whose argument slot is at
18
+ * `concretePath`, or undefined when this path is not one.
19
+ *
20
+ * Both halves have to line up: the enclosing object's schema must declare a ref
21
+ * slot whose `inputs` pointer names this field, and the manifest must fill that
22
+ * ref. Either missing means there is no call here to take arguments for.
23
+ */
24
+ export declare function callInputsAt(registry: AnalysisRegistry | undefined, analysis: ManifestAnalysis | undefined, docKind: string, resourceName: string | undefined, concretePath: string): Record<string, any> | undefined;
25
+ //# sourceMappingURL=call-inputs.d.ts.map