@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.
- 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/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/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/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/semantic-tokens/build-semantic-tokens.ts +84 -30
- package/src/types.ts +47 -6
|
@@ -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,
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a `Run` step is declared.
|
|
3
|
+
*
|
|
4
|
+
* The one CEL scope whose members really are written in the manifest but are
|
|
5
|
+
* reached through no reference slot — `steps.encode.result` names something a
|
|
6
|
+
* `!ref` resolver has never heard of. Finding it needs the declaring kind's own
|
|
7
|
+
* step-body annotation, which is why it goes through the scope query rather
|
|
8
|
+
* than through the graph navigation the other CEL roots use.
|
|
9
|
+
*/
|
|
10
|
+
import type { AstDocument, CelScopeQuery, LoadedFile, LoadedGraph } from "@telorun/analyzer";
|
|
11
|
+
import type { DefinitionResult } from "../types.js";
|
|
12
|
+
import { docIdentity } from "../doc-identity.js";
|
|
13
|
+
|
|
14
|
+
/** The loaded file for `filePath`, whose position index maps a manifest path to
|
|
15
|
+
* a source range. */
|
|
16
|
+
function loadedFile(graph: LoadedGraph, filePath: string): LoadedFile | undefined {
|
|
17
|
+
for (const mod of graph.modules.values()) {
|
|
18
|
+
if (mod.owner.source === filePath) return mod.owner;
|
|
19
|
+
const partial = mod.partials.find((p) => p.source === filePath);
|
|
20
|
+
if (partial) return partial;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function locateStepDeclaration(
|
|
26
|
+
graph: LoadedGraph,
|
|
27
|
+
filePath: string,
|
|
28
|
+
docs: AstDocument[],
|
|
29
|
+
docIndex: number,
|
|
30
|
+
stepName: 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 stepPath = scopeQuery.stepDeclarationPath(resource, stepName);
|
|
39
|
+
if (!stepPath) return undefined;
|
|
40
|
+
|
|
41
|
+
const file = loadedFile(graph, filePath);
|
|
42
|
+
const index = file?.positions[docIndex]?.positionIndex;
|
|
43
|
+
if (!index) return undefined;
|
|
44
|
+
// The step's own `name:` value is what the jump underlines — the step object's
|
|
45
|
+
// range would highlight the whole block, which reads as a selection rather
|
|
46
|
+
// than as a declaration.
|
|
47
|
+
const range =
|
|
48
|
+
index.get(`${stepPath}.name`) ?? index.get(`@key:${stepPath}.name`) ?? index.get(stepPath);
|
|
49
|
+
return range ? { uri: file!.source, range } : undefined;
|
|
50
|
+
}
|
|
@@ -55,11 +55,23 @@ function resolveResourceChain(
|
|
|
55
55
|
* analyzer reports the syntax error itself. Only that failure is tolerated: a
|
|
56
56
|
* defect in the CEL wrapper propagates rather than reading as "nothing to
|
|
57
57
|
* navigate to". */
|
|
58
|
+
export interface CelTargetResolvers {
|
|
59
|
+
/** Where a step of the CURRENT resource is declared. Supplied by the caller
|
|
60
|
+
* because finding one needs the declaring kind's step-body annotation, which
|
|
61
|
+
* the graph alone does not carry. */
|
|
62
|
+
locateStep?(stepName: string): DefinitionResult | undefined;
|
|
63
|
+
/** Where a context binding (`request.query`, `self.<field>`,
|
|
64
|
+
* `result.<field>`) was declared. Same reason: the site is derived by an
|
|
65
|
+
* `x-telo-context-*` annotation, which only the scope query can read. */
|
|
66
|
+
locateContextBinding?(parts: string[]): DefinitionResult | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
export function resolveCelTarget(
|
|
59
70
|
graph: LoadedGraph,
|
|
60
71
|
currentModule: LoadedModule,
|
|
61
72
|
segment: CelSegment,
|
|
62
73
|
offset: number,
|
|
74
|
+
resolvers?: CelTargetResolvers,
|
|
63
75
|
): DefinitionResult | undefined {
|
|
64
76
|
let ast: CelNode;
|
|
65
77
|
try {
|
|
@@ -80,5 +92,18 @@ export function resolveCelTarget(
|
|
|
80
92
|
return undefined;
|
|
81
93
|
}
|
|
82
94
|
if (root === "resources") return resolveResourceChain(graph, currentModule, parts, index);
|
|
95
|
+
// `steps.<name>` navigates to the step; `steps` itself and `.result` do not —
|
|
96
|
+
// the first names no one step, the second is the contract's output, which is
|
|
97
|
+
// declared by the invoked target rather than at the read site.
|
|
98
|
+
if (root === "steps" && index === 1 && resolvers?.locateStep) {
|
|
99
|
+
return resolvers.locateStep(parts[1].name);
|
|
100
|
+
}
|
|
101
|
+
// Anything else in scope came from an `x-telo-context-*` annotation, which
|
|
102
|
+
// names a real manifest node for `request` / `self` / `result` and friends.
|
|
103
|
+
// The chain UP TO the cursor is what resolves — hovering `query` in
|
|
104
|
+
// `request.query.lastEventId` navigates to `query`, not to the leaf.
|
|
105
|
+
if (index >= 1 && resolvers?.locateContextBinding) {
|
|
106
|
+
return resolvers.locateContextBinding(parts.slice(0, index + 1).map((p) => p.name));
|
|
107
|
+
}
|
|
83
108
|
return undefined;
|
|
84
109
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A document's `kind` + `metadata.name` — the pair that names the resource an
|
|
3
|
+
* analyzed manifest set holds it under.
|
|
4
|
+
*
|
|
5
|
+
* Its own module because it is a document-identity primitive, not a completion
|
|
6
|
+
* one: hover, semantic tokens and both declaration locators need it, and
|
|
7
|
+
* reaching it through the completion entry point dragged that whole module
|
|
8
|
+
* graph — CEL completion, call-input resolution, the import-source machinery —
|
|
9
|
+
* into surfaces that use none of it.
|
|
10
|
+
*/
|
|
11
|
+
import type { AstDocument, AstMap } from "@telorun/analyzer";
|
|
12
|
+
|
|
13
|
+
const scalar = (node: { kind: string; value?: unknown } | undefined): string | undefined =>
|
|
14
|
+
node?.kind === "scalar" && typeof node.value === "string" ? node.value : undefined;
|
|
15
|
+
|
|
16
|
+
/** Either half may be absent while the author is still writing the document. */
|
|
17
|
+
export function docIdentity(doc: AstDocument | undefined): { kind?: string; name?: string } {
|
|
18
|
+
if (doc?.root?.kind !== "map") return {};
|
|
19
|
+
let kind: string | undefined;
|
|
20
|
+
let name: string | undefined;
|
|
21
|
+
for (const pair of doc.root.entries) {
|
|
22
|
+
const key = scalar(pair.key);
|
|
23
|
+
if (key === "kind") kind = scalar(pair.value);
|
|
24
|
+
else if (key === "metadata" && pair.value?.kind === "map") {
|
|
25
|
+
const meta = pair.value as AstMap;
|
|
26
|
+
const nameEntry = meta.entries.find((e) => scalar(e.key) === "name");
|
|
27
|
+
name = scalar(nameEntry?.value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { kind, name };
|
|
31
|
+
}
|
package/src/hover/build-hover.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CelParseError,
|
|
2
3
|
parseToAst,
|
|
3
4
|
readRefSlot,
|
|
4
5
|
type AnalysisRegistry,
|
|
5
6
|
type AstDocument,
|
|
7
|
+
type CelNode,
|
|
8
|
+
type CelScopeQuery,
|
|
9
|
+
type ManifestAnalysis,
|
|
6
10
|
} from "@telorun/analyzer";
|
|
7
11
|
import type { HoverResult } from "../types.js";
|
|
12
|
+
import { chainAt } from "../cel-chain.js";
|
|
13
|
+
import { celSymbolAt } from "../cel/symbols.js";
|
|
14
|
+
import { docIdentity } from "../doc-identity.js";
|
|
8
15
|
import { navigateSchema } from "../completions/detect-context.js";
|
|
9
16
|
import {
|
|
10
17
|
resolveNodeAtPosition,
|
|
@@ -90,7 +97,9 @@ function fieldSchemaFor(
|
|
|
90
97
|
if (!resourceKind || !registry) return undefined;
|
|
91
98
|
const def = registry.resolveDefinition(resourceKind);
|
|
92
99
|
if (!def?.schema) return undefined;
|
|
93
|
-
return navigateSchema(def.schema as Record<string, any>, relativePath)
|
|
100
|
+
return navigateSchema(def.schema as Record<string, any>, relativePath, (from) =>
|
|
101
|
+
registry.resolveSchemaFrom(from, resourceKind),
|
|
102
|
+
);
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
export function buildHover(
|
|
@@ -99,15 +108,72 @@ export function buildHover(
|
|
|
99
108
|
character: number,
|
|
100
109
|
registry: AnalysisRegistry | undefined,
|
|
101
110
|
docs?: AstDocument[],
|
|
111
|
+
/** The host's analysis. Without it a CEL identifier hovers as nothing — its
|
|
112
|
+
* type is a property of the resolved scope, and there is no second source
|
|
113
|
+
* for it. */
|
|
114
|
+
analysis?: ManifestAnalysis,
|
|
102
115
|
): HoverResult | undefined {
|
|
103
116
|
const astDocs = docs ?? parseToAst(text);
|
|
104
117
|
const resolved = resolveNodeAtPosition(text, astDocs, line, character);
|
|
105
118
|
if (!resolved) return undefined;
|
|
106
119
|
|
|
120
|
+
if (resolved.cel) {
|
|
121
|
+
const hover = hoverForCel(resolved, astDocs, analysis?.celScope);
|
|
122
|
+
// A CEL body is still a field value; when the cursor is on nothing
|
|
123
|
+
// nameable inside it (an operator, a literal), fall through to the field's
|
|
124
|
+
// own hover rather than reporting nothing.
|
|
125
|
+
if (hover) return hover;
|
|
126
|
+
}
|
|
107
127
|
if (resolved.slot === "value") return hoverForValue(resolved, registry);
|
|
108
128
|
return hoverForKey(resolved, registry);
|
|
109
129
|
}
|
|
110
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Hover for one identifier of a CEL chain.
|
|
133
|
+
*
|
|
134
|
+
* The TYPE comes from the resolved scope; a DESCRIPTION comes from whatever
|
|
135
|
+
* schema node declared the name. Where to jump is the other half's answer
|
|
136
|
+
* (`resolveCelTarget`) and is deliberately not consulted here — hover must
|
|
137
|
+
* still say what `steps.encode.result` IS even though nothing in the manifest
|
|
138
|
+
* declares it.
|
|
139
|
+
*/
|
|
140
|
+
function hoverForCel(
|
|
141
|
+
resolved: ResolvedCursor,
|
|
142
|
+
docs: AstDocument[],
|
|
143
|
+
scopeQuery: CelScopeQuery | undefined,
|
|
144
|
+
): HoverResult | undefined {
|
|
145
|
+
if (!resolved.cel || !scopeQuery) return undefined;
|
|
146
|
+
const identity = docIdentity(docs[resolved.docIndex]);
|
|
147
|
+
const resource = scopeQuery.resourceFor(identity.kind, identity.name);
|
|
148
|
+
if (!resource) return undefined;
|
|
149
|
+
|
|
150
|
+
let ast: CelNode;
|
|
151
|
+
try {
|
|
152
|
+
ast = resolved.cel.segment.ast();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// An expression the author is still writing does not parse. That means
|
|
155
|
+
// there is no chain to hit-test, not an error to report from a hover — the
|
|
156
|
+
// analyzer reports the syntax error itself. Only that failure is tolerated.
|
|
157
|
+
if (!(error instanceof CelParseError)) throw error;
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const hit = chainAt(ast, resolved.cel.offset);
|
|
162
|
+
if (!hit) return undefined;
|
|
163
|
+
// The chain UP TO the cursor, not the whole chain: hovering `resources` in
|
|
164
|
+
// `resources.db.url` describes `resources`.
|
|
165
|
+
const parts = hit.parts.slice(0, hit.index + 1).map((p) => p.name);
|
|
166
|
+
const scope = scopeQuery.scopeAt(resource, resolved.concretePath ?? "");
|
|
167
|
+
const symbol = celSymbolAt(scope, parts);
|
|
168
|
+
if (!symbol) return undefined;
|
|
169
|
+
|
|
170
|
+
const lines = [symbol.type ? `**${symbol.name}**: \`${symbol.type}\`` : `**${symbol.name}**`];
|
|
171
|
+
if (symbol.description) lines.push("", symbol.description);
|
|
172
|
+
const chainText = parts.join(".");
|
|
173
|
+
if (chainText !== symbol.name) lines.push("", `\`${chainText}\``);
|
|
174
|
+
return { contents: lines.join("\n") };
|
|
175
|
+
}
|
|
176
|
+
|
|
111
177
|
function hoverForValue(
|
|
112
178
|
resolved: ResolvedCursor,
|
|
113
179
|
registry: AnalysisRegistry | undefined,
|