@telorun/ide-support 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cel/cursor-chain.d.ts +30 -0
- package/dist/cel/cursor-chain.d.ts.map +1 -0
- package/dist/cel/cursor-chain.js +30 -0
- package/dist/cel/symbols.d.ts +82 -0
- package/dist/cel/symbols.d.ts.map +1 -0
- package/dist/cel/symbols.js +147 -0
- package/dist/cel/tokens.d.ts +32 -0
- package/dist/cel/tokens.d.ts.map +1 -0
- package/dist/cel/tokens.js +162 -0
- package/dist/completions/build.d.ts +6 -2
- package/dist/completions/build.d.ts.map +1 -1
- package/dist/completions/build.js +52 -24
- package/dist/completions/call-inputs.d.ts +25 -0
- package/dist/completions/call-inputs.d.ts.map +1 -0
- package/dist/completions/call-inputs.js +78 -0
- package/dist/completions/cel-completions.d.ts +26 -0
- package/dist/completions/cel-completions.d.ts.map +1 -0
- package/dist/completions/cel-completions.js +78 -0
- package/dist/completions/detect-context.d.ts +47 -8
- package/dist/completions/detect-context.d.ts.map +1 -1
- package/dist/completions/detect-context.js +51 -15
- package/dist/completions/prop-keys.d.ts +5 -1
- package/dist/completions/prop-keys.d.ts.map +1 -1
- package/dist/completions/prop-keys.js +51 -3
- package/dist/completions/resolve-node.d.ts +9 -2
- package/dist/completions/resolve-node.d.ts.map +1 -1
- package/dist/completions/resolve-node.js +63 -21
- package/dist/completions/valid-capabilities.js +1 -1
- package/dist/definition/build-definition.d.ts +6 -2
- package/dist/definition/build-definition.d.ts.map +1 -1
- package/dist/definition/build-definition.js +16 -3
- package/dist/definition/locate-context-binding.d.ts +15 -0
- package/dist/definition/locate-context-binding.d.ts.map +1 -0
- package/dist/definition/locate-context-binding.js +35 -0
- package/dist/definition/locate-step.d.ts +13 -0
- package/dist/definition/locate-step.d.ts.map +1 -0
- package/dist/definition/locate-step.js +33 -0
- package/dist/definition/resolve-cel-target.d.ts +11 -1
- package/dist/definition/resolve-cel-target.d.ts.map +1 -1
- package/dist/definition/resolve-cel-target.js +14 -14
- package/dist/doc-identity.d.ts +17 -0
- package/dist/doc-identity.d.ts.map +1 -0
- package/dist/doc-identity.js +19 -0
- package/dist/hover/build-hover.d.ts +6 -2
- package/dist/hover/build-hover.d.ts.map +1 -1
- package/dist/hover/build-hover.js +64 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/semantic-tokens/build-semantic-tokens.d.ts +13 -8
- package/dist/semantic-tokens/build-semantic-tokens.d.ts.map +1 -1
- package/dist/semantic-tokens/build-semantic-tokens.js +81 -37
- package/dist/types.d.ts +25 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +16 -2
- package/package.json +2 -2
- package/src/cel/cursor-chain.ts +58 -0
- package/src/cel/symbols.ts +189 -0
- package/src/cel/tokens.ts +169 -0
- package/src/completions/build.ts +85 -22
- package/src/completions/call-inputs.ts +92 -0
- package/src/completions/cel-completions.ts +108 -0
- package/src/completions/detect-context.ts +107 -13
- package/src/completions/prop-keys.ts +59 -2
- package/src/completions/resolve-node.ts +82 -17
- package/src/completions/valid-capabilities.ts +1 -1
- package/src/definition/build-definition.ts +30 -2
- package/src/definition/locate-context-binding.ts +53 -0
- package/src/definition/locate-step.ts +50 -0
- package/src/definition/resolve-cel-target.ts +25 -0
- package/src/doc-identity.ts +31 -0
- package/src/hover/build-hover.ts +67 -1
- package/src/index.ts +4 -0
- package/src/semantic-tokens/build-semantic-tokens.ts +84 -30
- package/src/types.ts +47 -6
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
parseToAst,
|
|
3
|
+
readRefSlot,
|
|
4
|
+
refSlotAnnotation,
|
|
5
|
+
type AstDocument,
|
|
6
|
+
type CelSegment,
|
|
7
|
+
} from "@telorun/analyzer";
|
|
2
8
|
import type { ReplaceRange } from "../types.js";
|
|
3
9
|
import { resolveNodeAtPosition } from "./resolve-node.js";
|
|
4
10
|
|
|
@@ -19,7 +25,17 @@ export type CompletionCtx =
|
|
|
19
25
|
replaceRange: ReplaceRange;
|
|
20
26
|
}
|
|
21
27
|
| { type: "capability" }
|
|
22
|
-
| {
|
|
28
|
+
| {
|
|
29
|
+
type: "prop-key";
|
|
30
|
+
docKind: string;
|
|
31
|
+
yamlPath: string[];
|
|
32
|
+
/** The same location with sequence indices kept, so a caller can find the
|
|
33
|
+
* manifest node this slot sits in — what resolving an enclosing call's
|
|
34
|
+
* reference needs. */
|
|
35
|
+
concretePath: string;
|
|
36
|
+
docIndex: number;
|
|
37
|
+
existingKeys: Set<string>;
|
|
38
|
+
}
|
|
23
39
|
| {
|
|
24
40
|
/** Cursor sits on the value of an object-form ref's `name:` field
|
|
25
41
|
* (e.g. `connection: { kind: Sql.Connection, name: |}`). Editor hosts
|
|
@@ -43,6 +59,35 @@ export type CompletionCtx =
|
|
|
43
59
|
prefix: string;
|
|
44
60
|
/** Full source range of the value being completed. */
|
|
45
61
|
replaceRange: ReplaceRange;
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
/** Cursor sits on an ordinary field VALUE whose schema declares the values
|
|
65
|
+
* it may take — `enum` (closed) or `examples` (open). Untargeted on
|
|
66
|
+
* purpose: every value slot resolves here and the ones declaring neither
|
|
67
|
+
* simply offer nothing. */
|
|
68
|
+
type: "value-suggestions";
|
|
69
|
+
docKind: string;
|
|
70
|
+
/** Path from the document root to the field, so its schema can be found. */
|
|
71
|
+
yamlPath: string[];
|
|
72
|
+
replaceRange: ReplaceRange;
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
/** Cursor sits inside a CEL body — closed or still open (`!cel "req.|`).
|
|
76
|
+
* What completes is decided by the scope the analyzer resolves for this
|
|
77
|
+
* site, so the host must supply a `CelScopeQuery`; without one the
|
|
78
|
+
* candidate list would be a guess rather than a claim about what
|
|
79
|
+
* `telo check` accepts, and nothing is offered. */
|
|
80
|
+
type: "cel";
|
|
81
|
+
docKind?: string;
|
|
82
|
+
/** Which `---` document the cursor is in, so the host can name the
|
|
83
|
+
* resource this expression belongs to. */
|
|
84
|
+
docIndex: number;
|
|
85
|
+
/** The site's address with sequence indices kept — what the scope is
|
|
86
|
+
* resolved at. */
|
|
87
|
+
concretePath: string;
|
|
88
|
+
segment: CelSegment;
|
|
89
|
+
/** Cursor as a document offset. */
|
|
90
|
+
offset: number;
|
|
46
91
|
};
|
|
47
92
|
|
|
48
93
|
/** Returns every schema branch reachable from `node` after peeling `anyOf` /
|
|
@@ -116,19 +161,52 @@ function resolveLocalRef(
|
|
|
116
161
|
return current;
|
|
117
162
|
}
|
|
118
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Resolves an `x-telo-schema-from` annotation to the schema it derives. Supplied
|
|
166
|
+
* by the caller because the anchor is alias-qualified and only the registry can
|
|
167
|
+
* resolve it in the declaring kind's module scope.
|
|
168
|
+
*/
|
|
169
|
+
export type SchemaFromResolver = (schemaFrom: string) => Record<string, any> | undefined;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Expand a node whose shape comes from a sibling kind's schema.
|
|
173
|
+
*
|
|
174
|
+
* A slot annotated `x-telo-schema-from` declares NO `properties` of its own — an
|
|
175
|
+
* `Http.Api` route's `request:` is exactly this — so every walk that reads
|
|
176
|
+
* `properties` finds an empty node and silently offers nothing. The derived
|
|
177
|
+
* schema is merged UNDER whatever the slot itself declared, so a slot that adds
|
|
178
|
+
* a title or narrows a field keeps winning.
|
|
179
|
+
*/
|
|
180
|
+
function resolveSchemaFrom(
|
|
181
|
+
node: Record<string, any> | undefined,
|
|
182
|
+
resolve: SchemaFromResolver | undefined,
|
|
183
|
+
): Record<string, any> | undefined {
|
|
184
|
+
const from = node?.["x-telo-schema-from"];
|
|
185
|
+
if (!node || !resolve || typeof from !== "string") return node;
|
|
186
|
+
const derived = resolve(from);
|
|
187
|
+
if (!derived) return node;
|
|
188
|
+
return {
|
|
189
|
+
...derived,
|
|
190
|
+
...node,
|
|
191
|
+
properties: { ...(derived.properties ?? {}), ...(node.properties ?? {}) },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
119
195
|
/** Navigate a JSON Schema hierarchy following `path`, auto-descending into
|
|
120
|
-
* array items, peeling `anyOf` / `oneOf` branches
|
|
121
|
-
* `$ref`s. When multiple peeled
|
|
122
|
-
* synthetic node whose `properties` is
|
|
123
|
-
* and whose `required` is the
|
|
124
|
-
* surface every key a value at
|
|
196
|
+
* array items, peeling `anyOf` / `oneOf` branches, following document-local
|
|
197
|
+
* `$ref`s and expanding `x-telo-schema-from` slots. When multiple peeled
|
|
198
|
+
* branches define `properties`, returns a synthetic node whose `properties` is
|
|
199
|
+
* the union (first-wins on key collision) and whose `required` is the
|
|
200
|
+
* intersection — enough for propKeyCompletions to surface every key a value at
|
|
201
|
+
* this slot can legally carry. */
|
|
125
202
|
export function navigateSchema(
|
|
126
203
|
schema: Record<string, any>,
|
|
127
204
|
path: string[],
|
|
205
|
+
schemaFrom?: SchemaFromResolver,
|
|
128
206
|
): Record<string, any> | undefined {
|
|
129
207
|
let current: Record<string, any> | undefined = schema;
|
|
130
208
|
for (const segment of path) {
|
|
131
|
-
current = resolveLocalRef(current, schema);
|
|
209
|
+
current = resolveSchemaFrom(resolveLocalRef(current, schema), schemaFrom);
|
|
132
210
|
if (!current) return undefined;
|
|
133
211
|
const candidates = peelCombinators(current).flatMap((node) => {
|
|
134
212
|
const expanded: Record<string, any>[] = [];
|
|
@@ -161,7 +239,7 @@ export function navigateSchema(
|
|
|
161
239
|
if (!next) return undefined;
|
|
162
240
|
current = next;
|
|
163
241
|
}
|
|
164
|
-
current = resolveLocalRef(current, schema);
|
|
242
|
+
current = resolveSchemaFrom(resolveLocalRef(current, schema), schemaFrom);
|
|
165
243
|
if (!current) return undefined;
|
|
166
244
|
// Auto-descend through a trailing array at the leaf (e.g. cursor inside `mounts:` items)
|
|
167
245
|
while (current.type === "array" && current.items) {
|
|
@@ -227,8 +305,9 @@ function unionLeaves(
|
|
|
227
305
|
export function lookupRefConstraints(
|
|
228
306
|
definitionSchema: Record<string, any>,
|
|
229
307
|
yamlPath: string[],
|
|
308
|
+
schemaFrom?: SchemaFromResolver,
|
|
230
309
|
): string[] {
|
|
231
|
-
const node = navigateSchema(definitionSchema, yamlPath);
|
|
310
|
+
const node = navigateSchema(definitionSchema, yamlPath, schemaFrom);
|
|
232
311
|
if (!node) return [];
|
|
233
312
|
return readRefSlot(node)?.kinds ?? [];
|
|
234
313
|
}
|
|
@@ -249,9 +328,18 @@ export function detectContext(
|
|
|
249
328
|
const { docKind } = resolved;
|
|
250
329
|
|
|
251
330
|
if (resolved.slot === "value") {
|
|
252
|
-
// Inside a CEL body — structural completion does not apply
|
|
253
|
-
//
|
|
254
|
-
if (resolved.cel)
|
|
331
|
+
// Inside a CEL body — structural completion does not apply; what completes
|
|
332
|
+
// are the names the expression may use.
|
|
333
|
+
if (resolved.cel) {
|
|
334
|
+
return {
|
|
335
|
+
type: "cel",
|
|
336
|
+
docKind,
|
|
337
|
+
docIndex: resolved.docIndex,
|
|
338
|
+
concretePath: resolved.concretePath ?? "",
|
|
339
|
+
segment: resolved.cel.segment,
|
|
340
|
+
offset: resolved.cel.offset,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
255
343
|
const replaceRange = resolved.replaceRange;
|
|
256
344
|
if (!replaceRange) return undefined;
|
|
257
345
|
const key = resolved.path[resolved.path.length - 1];
|
|
@@ -297,6 +385,10 @@ export function detectContext(
|
|
|
297
385
|
}
|
|
298
386
|
}
|
|
299
387
|
|
|
388
|
+
if (docKind) {
|
|
389
|
+
return { type: "value-suggestions", docKind, yamlPath: resolved.path, replaceRange };
|
|
390
|
+
}
|
|
391
|
+
|
|
300
392
|
return undefined;
|
|
301
393
|
}
|
|
302
394
|
|
|
@@ -311,6 +403,8 @@ export function detectContext(
|
|
|
311
403
|
type: "prop-key",
|
|
312
404
|
docKind: scopeKind,
|
|
313
405
|
yamlPath: resolved.path.slice(resolved.resourceDepth ?? 0),
|
|
406
|
+
concretePath: resolved.concretePath ?? "",
|
|
407
|
+
docIndex: resolved.docIndex,
|
|
314
408
|
existingKeys: resolved.existingKeys ?? new Set<string>(),
|
|
315
409
|
};
|
|
316
410
|
}
|
|
@@ -25,7 +25,18 @@ export function propKeyCompletions(
|
|
|
25
25
|
yamlPath: string[],
|
|
26
26
|
existingKeys: Set<string>,
|
|
27
27
|
registry: AnalysisRegistry | undefined,
|
|
28
|
+
/** The arguments the enclosing call declares, when this path IS that call's
|
|
29
|
+
* argument slot. Resolved by the caller, which is the side holding the
|
|
30
|
+
* manifest the reference names. */
|
|
31
|
+
callInputs?: Record<string, any>,
|
|
28
32
|
): CompletionResult[] {
|
|
33
|
+
if (callInputs?.properties) {
|
|
34
|
+
return buildItems(
|
|
35
|
+
callInputs.properties as Record<string, any>,
|
|
36
|
+
existingKeys,
|
|
37
|
+
new Set<string>(Array.isArray(callInputs.required) ? callInputs.required : []),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
29
40
|
if (!registry) return [];
|
|
30
41
|
|
|
31
42
|
const definition = registry.resolveDefinition(kind);
|
|
@@ -41,9 +52,20 @@ export function propKeyCompletions(
|
|
|
41
52
|
|
|
42
53
|
const targetSchema = yamlPath.length === 0
|
|
43
54
|
? (definition.schema as Record<string, any>)
|
|
44
|
-
: navigateSchema(definition.schema as Record<string, any>, yamlPath)
|
|
55
|
+
: navigateSchema(definition.schema as Record<string, any>, yamlPath, (from) =>
|
|
56
|
+
registry.resolveSchemaFrom(from, kind),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// A NAME-KEYED map declares its keys nowhere in `properties` — they are the
|
|
60
|
+
// author's own (a media type, a header name). `propertyNames` is JSON
|
|
61
|
+
// Schema's own vocabulary for what they may be, and it carries the open/closed
|
|
62
|
+
// distinction already: `enum` constrains, `examples` only suggests. So an open
|
|
63
|
+
// list of known values needs no annotation and no analyzer-side knowledge of
|
|
64
|
+
// any domain.
|
|
65
|
+
const keySuggestions = propertyNameSuggestions(targetSchema, existingKeys);
|
|
45
66
|
|
|
46
67
|
if (!targetSchema?.properties) {
|
|
68
|
+
if (keySuggestions.length > 0) return keySuggestions;
|
|
47
69
|
if (yamlPath.length === 0) {
|
|
48
70
|
return buildItems(ROOT_IMPLICIT_PROPS, existingKeys, new Set<string>());
|
|
49
71
|
}
|
|
@@ -58,7 +80,42 @@ export function propKeyCompletions(
|
|
|
58
80
|
? { ...ROOT_IMPLICIT_PROPS, ...(targetSchema.properties as Record<string, any>) }
|
|
59
81
|
: { ...(targetSchema.properties as Record<string, any>), ...annotationKeys(targetSchema) };
|
|
60
82
|
|
|
61
|
-
return buildItems(properties, existingKeys, required);
|
|
83
|
+
return [...keySuggestions, ...buildItems(properties, existingKeys, required)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Key candidates a map-valued node declares through `propertyNames`.
|
|
88
|
+
*
|
|
89
|
+
* `enum` is a closed set and `examples` an open one — suggestions with no
|
|
90
|
+
* validation effect, which is exactly "these are the known values, others are
|
|
91
|
+
* allowed". Both are stock JSON Schema, so nothing here knows what a media type
|
|
92
|
+
* is and any name-keyed field gains the same behaviour by declaring it.
|
|
93
|
+
*/
|
|
94
|
+
function propertyNameSuggestions(
|
|
95
|
+
schema: Record<string, any> | undefined,
|
|
96
|
+
existingKeys: Set<string>,
|
|
97
|
+
): CompletionResult[] {
|
|
98
|
+
const names = schema?.propertyNames as Record<string, any> | undefined;
|
|
99
|
+
if (!names || typeof names !== "object") return [];
|
|
100
|
+
const closed = Array.isArray(names.enum) ? (names.enum as unknown[]) : undefined;
|
|
101
|
+
const open = Array.isArray(names.examples) ? (names.examples as unknown[]) : [];
|
|
102
|
+
const values = closed ?? open;
|
|
103
|
+
|
|
104
|
+
const out: CompletionResult[] = [];
|
|
105
|
+
for (const value of values) {
|
|
106
|
+
if (typeof value !== "string" || existingKeys.has(value)) continue;
|
|
107
|
+
out.push({
|
|
108
|
+
label: value,
|
|
109
|
+
kind: "enumMember",
|
|
110
|
+
insertText: `${value}: $0`,
|
|
111
|
+
snippet: true,
|
|
112
|
+
detail: closed ? names.title ?? "allowed key" : names.title ?? "known key",
|
|
113
|
+
// Ahead of any structural key at the same level: at a name-keyed slot the
|
|
114
|
+
// author is choosing one of these, not adding a sibling field.
|
|
115
|
+
sortText: `0_${value}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
62
119
|
}
|
|
63
120
|
|
|
64
121
|
/**
|
|
@@ -48,8 +48,15 @@ export interface ResolvedCursor {
|
|
|
48
48
|
/** Key slot: number of `path` segments that reach `resourceKind`'s map, so
|
|
49
49
|
* the schema-relative path is `path.slice(resourceDepth)`. */
|
|
50
50
|
resourceDepth?: number;
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
51
|
+
/** The path with sequence INDICES kept (`routes[0].handler.url`),
|
|
52
|
+
* as distinct from `path`, through which arrays are transparent. This is the
|
|
53
|
+
* address the analyzer speaks — an `x-telo-context` scope, an error-bearing
|
|
54
|
+
* region and a step's identity are all resolved per item — so it is what a
|
|
55
|
+
* CEL site is looked up by. */
|
|
56
|
+
concretePath?: string;
|
|
57
|
+
/** Set when the cursor sits inside a CEL body (closed or open). The CEL
|
|
58
|
+
* segment plus the cursor's document offset — what completion and hover
|
|
59
|
+
* hit-test the expression's chain against. */
|
|
53
60
|
cel?: { segment: CelSegment; offset: number };
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -109,6 +116,7 @@ type Descent =
|
|
|
109
116
|
type: "key";
|
|
110
117
|
container: AstMap;
|
|
111
118
|
path: string[];
|
|
119
|
+
concretePath: string;
|
|
112
120
|
keyNode: AstScalar;
|
|
113
121
|
keyName?: string;
|
|
114
122
|
scope: ResourceScope;
|
|
@@ -119,15 +127,22 @@ type Descent =
|
|
|
119
127
|
* (which has no keyed siblings). */
|
|
120
128
|
container: AstMap | undefined;
|
|
121
129
|
path: string[];
|
|
130
|
+
concretePath: string;
|
|
122
131
|
keyName?: string;
|
|
123
132
|
keyEnd: number;
|
|
124
133
|
valueNode: AstNode;
|
|
125
134
|
}
|
|
126
135
|
| { type: "empty" };
|
|
127
136
|
|
|
137
|
+
/** Append one key segment to a concrete path (`routes[0]` + `handler`). */
|
|
138
|
+
function joinKey(concrete: string, key: string): string {
|
|
139
|
+
return concrete ? `${concrete}.${key}` : key;
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
function descend(
|
|
129
143
|
node: AstNode,
|
|
130
144
|
ancestors: string[],
|
|
145
|
+
concrete: string,
|
|
131
146
|
offset: number,
|
|
132
147
|
scope: ResourceScope,
|
|
133
148
|
): Descent | undefined {
|
|
@@ -140,6 +155,7 @@ function descend(
|
|
|
140
155
|
type: "key",
|
|
141
156
|
container: node,
|
|
142
157
|
path: ancestors,
|
|
158
|
+
concretePath: concrete,
|
|
143
159
|
keyNode: pair.key as AstScalar,
|
|
144
160
|
keyName,
|
|
145
161
|
scope: mapScope,
|
|
@@ -147,13 +163,15 @@ function descend(
|
|
|
147
163
|
}
|
|
148
164
|
if (pair.value && within(pair.value.range, offset)) {
|
|
149
165
|
const childAncestors = keyName != null ? [...ancestors, keyName] : ancestors;
|
|
166
|
+
const childConcrete = keyName != null ? joinKey(concrete, keyName) : concrete;
|
|
150
167
|
if (pair.value.kind === "map" || pair.value.kind === "seq") {
|
|
151
|
-
return descend(pair.value, childAncestors, offset, mapScope) ?? { type: "empty" };
|
|
168
|
+
return descend(pair.value, childAncestors, childConcrete, offset, mapScope) ?? { type: "empty" };
|
|
152
169
|
}
|
|
153
170
|
return {
|
|
154
171
|
type: "value",
|
|
155
172
|
container: node,
|
|
156
173
|
path: ancestors,
|
|
174
|
+
concretePath: childConcrete,
|
|
157
175
|
keyName,
|
|
158
176
|
keyEnd: pair.key.range[1],
|
|
159
177
|
valueNode: pair.value,
|
|
@@ -164,16 +182,26 @@ function descend(
|
|
|
164
182
|
}
|
|
165
183
|
if (node.kind === "seq") {
|
|
166
184
|
// Sequence items are transparent to the key path (mirrors the schema
|
|
167
|
-
// walker, which auto-descends arrays)
|
|
168
|
-
|
|
185
|
+
// walker, which auto-descends arrays) but NOT to the concrete path: an
|
|
186
|
+
// `x-telo-context` scope, an error-bearing region and a step's identity are
|
|
187
|
+
// all addressed per item, so a CEL site is unreachable without the index.
|
|
188
|
+
for (const [index, item] of node.items.entries()) {
|
|
169
189
|
if (within(item.range, offset)) {
|
|
190
|
+
const itemConcrete = `${concrete}[${index}]`;
|
|
170
191
|
if (item.kind === "map" || item.kind === "seq") {
|
|
171
|
-
return descend(item, ancestors, offset, scope) ?? { type: "empty" };
|
|
192
|
+
return descend(item, ancestors, itemConcrete, offset, scope) ?? { type: "empty" };
|
|
172
193
|
}
|
|
173
194
|
// A bare scalar list item (`targets:\n - One`) has no enclosing map of
|
|
174
195
|
// keyed siblings — leave `container` undefined rather than treating the
|
|
175
196
|
// seq as a map.
|
|
176
|
-
return {
|
|
197
|
+
return {
|
|
198
|
+
type: "value",
|
|
199
|
+
container: undefined,
|
|
200
|
+
path: ancestors,
|
|
201
|
+
concretePath: itemConcrete,
|
|
202
|
+
keyEnd: item.range[0],
|
|
203
|
+
valueNode: item,
|
|
204
|
+
};
|
|
177
205
|
}
|
|
178
206
|
}
|
|
179
207
|
return undefined;
|
|
@@ -187,6 +215,9 @@ function descend(
|
|
|
187
215
|
|
|
188
216
|
interface MapScope {
|
|
189
217
|
path: string[];
|
|
218
|
+
/** The same location with sequence indices kept — what addresses a manifest
|
|
219
|
+
* node, as distinct from `path`, which addresses a schema node. */
|
|
220
|
+
concrete: string;
|
|
190
221
|
childColumn: number;
|
|
191
222
|
keys: Set<string>;
|
|
192
223
|
rangeStart: number;
|
|
@@ -195,6 +226,7 @@ interface MapScope {
|
|
|
195
226
|
|
|
196
227
|
interface PairScope {
|
|
197
228
|
path: string[]; // full key path to this pair
|
|
229
|
+
concrete: string;
|
|
198
230
|
keyColumn: number;
|
|
199
231
|
keyOffset: number;
|
|
200
232
|
childKeys: Set<string>;
|
|
@@ -204,6 +236,7 @@ interface PairScope {
|
|
|
204
236
|
function collectScopes(
|
|
205
237
|
node: AstNode,
|
|
206
238
|
ancestors: string[],
|
|
239
|
+
concrete: string,
|
|
207
240
|
scope: ResourceScope,
|
|
208
241
|
lineOffsets: number[],
|
|
209
242
|
maps: MapScope[],
|
|
@@ -219,11 +252,19 @@ function collectScopes(
|
|
|
219
252
|
if (childColumn < 0) childColumn = offsetToPosition(pair.key.range[0], lineOffsets).character;
|
|
220
253
|
}
|
|
221
254
|
if (childColumn >= 0) {
|
|
222
|
-
maps.push({
|
|
255
|
+
maps.push({
|
|
256
|
+
path: ancestors,
|
|
257
|
+
concrete,
|
|
258
|
+
childColumn,
|
|
259
|
+
keys,
|
|
260
|
+
rangeStart: node.range[0],
|
|
261
|
+
scope: mapScope,
|
|
262
|
+
});
|
|
223
263
|
}
|
|
224
264
|
for (const pair of node.entries) {
|
|
225
265
|
const keyName = scalarString(pair.key);
|
|
226
266
|
const fullPath = keyName != null ? [...ancestors, keyName] : ancestors;
|
|
267
|
+
const fullConcrete = keyName != null ? joinKey(concrete, keyName) : concrete;
|
|
227
268
|
const childKeys = new Set<string>();
|
|
228
269
|
if (pair.value?.kind === "map") {
|
|
229
270
|
for (const p of pair.value.entries) {
|
|
@@ -233,20 +274,26 @@ function collectScopes(
|
|
|
233
274
|
}
|
|
234
275
|
pairs.push({
|
|
235
276
|
path: fullPath,
|
|
277
|
+
concrete: fullConcrete,
|
|
236
278
|
keyColumn: offsetToPosition(pair.key.range[0], lineOffsets).character,
|
|
237
279
|
keyOffset: pair.key.range[0],
|
|
238
280
|
childKeys,
|
|
239
281
|
scope: mapScope,
|
|
240
282
|
});
|
|
241
|
-
if (pair.value)
|
|
283
|
+
if (pair.value) {
|
|
284
|
+
collectScopes(pair.value, fullPath, fullConcrete, mapScope, lineOffsets, maps, pairs);
|
|
285
|
+
}
|
|
242
286
|
}
|
|
243
287
|
} else if (node.kind === "seq") {
|
|
244
|
-
|
|
288
|
+
node.items.forEach((item, index) =>
|
|
289
|
+
collectScopes(item, ancestors, `${concrete}[${index}]`, scope, lineOffsets, maps, pairs),
|
|
290
|
+
);
|
|
245
291
|
}
|
|
246
292
|
}
|
|
247
293
|
|
|
248
294
|
interface KeyResolution {
|
|
249
295
|
path: string[];
|
|
296
|
+
concrete: string;
|
|
250
297
|
existingKeys: Set<string>;
|
|
251
298
|
scope: ResourceScope;
|
|
252
299
|
}
|
|
@@ -262,7 +309,7 @@ function columnSearch(
|
|
|
262
309
|
): KeyResolution {
|
|
263
310
|
const maps: MapScope[] = [];
|
|
264
311
|
const pairs: PairScope[] = [];
|
|
265
|
-
collectScopes(root, [], { depth: 0 }, lineOffsets, maps, pairs);
|
|
312
|
+
collectScopes(root, [], "", { depth: 0 }, lineOffsets, maps, pairs);
|
|
266
313
|
|
|
267
314
|
// Sibling level: a map whose children already sit at the cursor's column.
|
|
268
315
|
let sibling: MapScope | undefined;
|
|
@@ -271,7 +318,14 @@ function columnSearch(
|
|
|
271
318
|
if (!sibling || m.rangeStart > sibling.rangeStart) sibling = m;
|
|
272
319
|
}
|
|
273
320
|
}
|
|
274
|
-
if (sibling)
|
|
321
|
+
if (sibling) {
|
|
322
|
+
return {
|
|
323
|
+
path: sibling.path,
|
|
324
|
+
concrete: sibling.concrete,
|
|
325
|
+
existingKeys: sibling.keys,
|
|
326
|
+
scope: sibling.scope,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
275
329
|
|
|
276
330
|
// Nest under the nearest-preceding key shallower than the cursor.
|
|
277
331
|
let nest: PairScope | undefined;
|
|
@@ -286,9 +340,16 @@ function columnSearch(
|
|
|
286
340
|
}
|
|
287
341
|
}
|
|
288
342
|
}
|
|
289
|
-
if (nest)
|
|
343
|
+
if (nest) {
|
|
344
|
+
return {
|
|
345
|
+
path: nest.path,
|
|
346
|
+
concrete: nest.concrete,
|
|
347
|
+
existingKeys: nest.childKeys,
|
|
348
|
+
scope: nest.scope,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
290
351
|
|
|
291
|
-
return { path: [], existingKeys: new Set(), scope: { depth: 0 } };
|
|
352
|
+
return { path: [], concrete: "", existingKeys: new Set(), scope: { depth: 0 } };
|
|
292
353
|
}
|
|
293
354
|
|
|
294
355
|
// ---------------------------------------------------------------------------
|
|
@@ -326,7 +387,7 @@ export function resolveNodeAtPosition(
|
|
|
326
387
|
const doc = docs[docIndex];
|
|
327
388
|
const docKind = docKindOf(doc);
|
|
328
389
|
|
|
329
|
-
const found = doc.root ? descend(doc.root, [], offset, { depth: 0 }) : undefined;
|
|
390
|
+
const found = doc.root ? descend(doc.root, [], "", offset, { depth: 0 }) : undefined;
|
|
330
391
|
|
|
331
392
|
// Cursor sits on an existing map key → key/prop-key position.
|
|
332
393
|
if (found?.type === "key") {
|
|
@@ -341,6 +402,7 @@ export function resolveNodeAtPosition(
|
|
|
341
402
|
docKind,
|
|
342
403
|
slot: "key",
|
|
343
404
|
path: found.path,
|
|
405
|
+
concretePath: found.concretePath,
|
|
344
406
|
node: found.keyNode,
|
|
345
407
|
replaceRange: { start: toPos(found.keyNode.range[0]), end: toPos(found.keyNode.range[1]) },
|
|
346
408
|
container: found.container,
|
|
@@ -365,13 +427,14 @@ export function resolveNodeAtPosition(
|
|
|
365
427
|
toPos(value.range[0]).line !== toPos(found.keyEnd).line;
|
|
366
428
|
if (isPartialKey && doc.root) {
|
|
367
429
|
const col = toPos(value.range[0]).character;
|
|
368
|
-
const { path, existingKeys, scope } = columnSearch(doc.root, col, offset, lineOffsets);
|
|
430
|
+
const { path, concrete, existingKeys, scope } = columnSearch(doc.root, col, offset, lineOffsets);
|
|
369
431
|
return {
|
|
370
432
|
docIndex,
|
|
371
433
|
offset,
|
|
372
434
|
docKind,
|
|
373
435
|
slot: "key",
|
|
374
436
|
path,
|
|
437
|
+
concretePath: concrete,
|
|
375
438
|
container: found.container,
|
|
376
439
|
existingKeys,
|
|
377
440
|
resourceKind: scope.kind,
|
|
@@ -386,6 +449,7 @@ export function resolveNodeAtPosition(
|
|
|
386
449
|
docKind,
|
|
387
450
|
slot: "value",
|
|
388
451
|
path: found.keyName != null ? [...found.path, found.keyName] : found.path,
|
|
452
|
+
concretePath: found.concretePath,
|
|
389
453
|
node: value,
|
|
390
454
|
container: found.container,
|
|
391
455
|
prefix: text.slice(value.range[0], clampedEnd),
|
|
@@ -400,13 +464,14 @@ export function resolveNodeAtPosition(
|
|
|
400
464
|
// resolved by cursor column.
|
|
401
465
|
const resolution: KeyResolution = doc.root
|
|
402
466
|
? columnSearch(doc.root, character, offset, lineOffsets)
|
|
403
|
-
: { path: [], existingKeys: new Set<string>(), scope: { depth: 0 } };
|
|
467
|
+
: { path: [], concrete: "", existingKeys: new Set<string>(), scope: { depth: 0 } };
|
|
404
468
|
return {
|
|
405
469
|
docIndex,
|
|
406
470
|
offset,
|
|
407
471
|
docKind,
|
|
408
472
|
slot: "key",
|
|
409
473
|
path: resolution.path,
|
|
474
|
+
concretePath: resolution.concrete,
|
|
410
475
|
existingKeys: resolution.existingKeys,
|
|
411
476
|
resourceKind: resolution.scope.kind,
|
|
412
477
|
resourceDepth: resolution.scope.depth,
|
|
@@ -10,7 +10,7 @@ export const CAPABILITY_VALUES = [
|
|
|
10
10
|
/** One-line role summary per capability, surfaced on hover. Kept in sync with
|
|
11
11
|
* the capability list in `CLAUDE.md` / the kernel builtins. */
|
|
12
12
|
export const CAPABILITY_DOCS: Record<string, string> = {
|
|
13
|
-
"Telo.Service": "Long-lived resource: `init()` +
|
|
13
|
+
"Telo.Service": "Long-lived resource: `init()` + `run()`, returning what undoes them (servers, pools).",
|
|
14
14
|
"Telo.Runnable": "One-shot task: `run()` (pipelines, boot steps).",
|
|
15
15
|
"Telo.Invocable": "Request handler: `invoke(inputs)` (scripts, endpoints).",
|
|
16
16
|
"Telo.Provider": "Value-flow source: `init()` + optional `provide()` (config, secrets).",
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
parseToAst,
|
|
3
|
+
type AstDocument,
|
|
4
|
+
type AstScalar,
|
|
5
|
+
type ManifestAnalysis,
|
|
6
|
+
type LoadedGraph,
|
|
7
|
+
} from "@telorun/analyzer";
|
|
2
8
|
import type { DefinitionResult } from "../types.js";
|
|
3
9
|
import { resolveNodeAtPosition } from "../completions/resolve-node.js";
|
|
4
10
|
import { splitAliasQualified } from "./alias-qualified-value.js";
|
|
11
|
+
import { locateContextBinding } from "./locate-context-binding.js";
|
|
12
|
+
import { locateStepDeclaration } from "./locate-step.js";
|
|
5
13
|
import { moduleForFile } from "./manifest-navigation.js";
|
|
6
14
|
import { resolveCelTarget } from "./resolve-cel-target.js";
|
|
7
15
|
import { isKindSlot, resolveKindTarget } from "./resolve-kind-target.js";
|
|
@@ -30,6 +38,10 @@ export function buildDefinition(
|
|
|
30
38
|
graph: LoadedGraph,
|
|
31
39
|
currentFilePath: string,
|
|
32
40
|
docs?: AstDocument[],
|
|
41
|
+
/** Lets a `steps.<name>` read navigate to the step that produced it, and a
|
|
42
|
+
* context binding to its declaration — both need the declaring kind's own
|
|
43
|
+
* annotations, which only the analysis can read. */
|
|
44
|
+
analysis?: ManifestAnalysis,
|
|
33
45
|
): DefinitionResult | undefined {
|
|
34
46
|
const astDocs = docs ?? parseToAst(text);
|
|
35
47
|
const resolved = resolveNodeAtPosition(text, astDocs, line, character);
|
|
@@ -38,7 +50,23 @@ export function buildDefinition(
|
|
|
38
50
|
const currentModule = moduleForFile(graph, currentFilePath) ?? graph.entry;
|
|
39
51
|
|
|
40
52
|
if (resolved.cel) {
|
|
41
|
-
return resolveCelTarget(graph, currentModule, resolved.cel.segment, resolved.cel.offset
|
|
53
|
+
return resolveCelTarget(graph, currentModule, resolved.cel.segment, resolved.cel.offset, {
|
|
54
|
+
// A step is resolved in the CURRENT document — `steps.<name>.result` is
|
|
55
|
+
// readable only inside the resource that declares it, which is one
|
|
56
|
+
// document. Supplied as a closure so the chain resolver stays free of the
|
|
57
|
+
// AST and the scope query alike.
|
|
58
|
+
locateStep: (stepName) =>
|
|
59
|
+
locateStepDeclaration(graph, currentFilePath, astDocs, resolved.docIndex, stepName, analysis?.celScope),
|
|
60
|
+
locateContextBinding: (parts) =>
|
|
61
|
+
locateContextBinding(
|
|
62
|
+
graph,
|
|
63
|
+
astDocs,
|
|
64
|
+
resolved.docIndex,
|
|
65
|
+
resolved.concretePath ?? "",
|
|
66
|
+
parts,
|
|
67
|
+
analysis?.celScope,
|
|
68
|
+
),
|
|
69
|
+
});
|
|
42
70
|
}
|
|
43
71
|
|
|
44
72
|
const node = resolved.node;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a CEL context binding was declared.
|
|
3
|
+
*
|
|
4
|
+
* `request.query` is written in the route's own `request.schema`, `self.<field>`
|
|
5
|
+
* in the definition's `schema`, `result.<field>` in the INVOKED resource's
|
|
6
|
+
* `outputType` — each derived by an `x-telo-context-*` annotation rather than
|
|
7
|
+
* reached through a reference slot, so none of them is navigable by the graph
|
|
8
|
+
* walk the other CEL roots use. The scope query resolves the annotation to a
|
|
9
|
+
* manifest identity plus a path; locating that path in the loaded files is what
|
|
10
|
+
* this adds.
|
|
11
|
+
*/
|
|
12
|
+
import type { AstDocument, CelScopeQuery, LoadedFile, LoadedGraph } from "@telorun/analyzer";
|
|
13
|
+
import type { DefinitionResult } from "../types.js";
|
|
14
|
+
import { docIdentity } from "../doc-identity.js";
|
|
15
|
+
|
|
16
|
+
/** Every file of every module in the graph — a binding's declaration may sit in
|
|
17
|
+
* another module (an invoked handler's `outputType`), so the search is not
|
|
18
|
+
* confined to the current file. */
|
|
19
|
+
function allFiles(graph: LoadedGraph): LoadedFile[] {
|
|
20
|
+
const out: LoadedFile[] = [];
|
|
21
|
+
for (const mod of graph.modules.values()) out.push(mod.owner, ...mod.partials);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function locateContextBinding(
|
|
26
|
+
graph: LoadedGraph,
|
|
27
|
+
docs: AstDocument[],
|
|
28
|
+
docIndex: number,
|
|
29
|
+
sitePath: string,
|
|
30
|
+
parts: string[],
|
|
31
|
+
scopeQuery: CelScopeQuery | undefined,
|
|
32
|
+
): DefinitionResult | undefined {
|
|
33
|
+
if (!scopeQuery) return undefined;
|
|
34
|
+
const identity = docIdentity(docs[docIndex]);
|
|
35
|
+
const resource = scopeQuery.resourceFor(identity.kind, identity.name);
|
|
36
|
+
if (!resource) return undefined;
|
|
37
|
+
|
|
38
|
+
const site = scopeQuery.contextDeclarationSite(resource, sitePath, parts);
|
|
39
|
+
if (!site) return undefined;
|
|
40
|
+
|
|
41
|
+
for (const file of allFiles(graph)) {
|
|
42
|
+
for (let i = 0; i < file.manifests.length; i++) {
|
|
43
|
+
const manifest = file.manifests[i] as { kind?: string; metadata?: { name?: string } } | null;
|
|
44
|
+
if (manifest?.kind !== site.kind || manifest.metadata?.name !== site.name) continue;
|
|
45
|
+
const index = file.positions[i]?.positionIndex;
|
|
46
|
+
// The KEY span, so the jump underlines `query:` rather than the block that
|
|
47
|
+
// follows it — a declaration, not a selection.
|
|
48
|
+
const range = index?.get(`@key:${site.path}`) ?? index?.get(site.path);
|
|
49
|
+
if (range) return { uri: file.source, range };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|