@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
|
@@ -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,
|
package/src/index.ts
CHANGED
|
@@ -6,3 +6,7 @@ export * from "./semantic-tokens/index.js";
|
|
|
6
6
|
export * from "./definition/index.js";
|
|
7
7
|
export * from "./rename/index.js";
|
|
8
8
|
export * from "./import-upgrades/index.js";
|
|
9
|
+
// The repo's single CEL-tree walk. Exported because every host that has to
|
|
10
|
+
// answer "where is this name read" needs it and a second copy would be a second
|
|
11
|
+
// answer — the editor asks it before deleting a resource.
|
|
12
|
+
export { walkCel, flattenChain, chainAt, type ChainPart } from "./cel-chain.js";
|
|
@@ -5,60 +5,114 @@ import {
|
|
|
5
5
|
type AnalysisRegistry,
|
|
6
6
|
type AstDocument,
|
|
7
7
|
type AstNode,
|
|
8
|
+
type AstScalar,
|
|
9
|
+
type CelScope,
|
|
10
|
+
type ManifestAnalysis,
|
|
8
11
|
} from "@telorun/analyzer";
|
|
9
12
|
import type { SemanticToken } from "../types.js";
|
|
13
|
+
import { celSegmentTokens } from "../cel/tokens.js";
|
|
14
|
+
import { docIdentity } from "../doc-identity.js";
|
|
10
15
|
import { scalarString } from "../completions/resolve-node.js";
|
|
11
16
|
import { CAPABILITY_VALUES } from "../completions/valid-capabilities.js";
|
|
12
17
|
|
|
13
18
|
const CAPABILITIES = new Set<string>(CAPABILITY_VALUES);
|
|
14
19
|
|
|
20
|
+
/** Append one key segment to a concrete path (`routes[0]` + `handler`). */
|
|
21
|
+
function joinKey(concrete: string, key: string): string {
|
|
22
|
+
return concrete ? `${concrete}.${key}` : key;
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
/** Registry-aware semantic tokens: a `kind:` value that resolves to a known
|
|
16
26
|
* definition is a `type`; a `capability:` value is an `interface`; a `!ref`
|
|
17
|
-
* target is a `variable`.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
27
|
+
* target is a `variable`. Ref targets are colored here rather than in the
|
|
28
|
+
* grammar because a `!ref` after a `key:` is tokenized by the bundled YAML
|
|
29
|
+
* grammar before a Telo pattern can claim it — the AST sees it unambiguously.
|
|
30
|
+
* An unresolved kind gets no token, so a typo stays uncolored — a quiet signal
|
|
31
|
+
* that pairs with the analyzer's `UNDEFINED_KIND` diagnostic.
|
|
32
|
+
*
|
|
33
|
+
* The inside of a `!cel` / `${{ }}` body is colored here too, and for the same
|
|
34
|
+
* reason one level down: a grammar can only know the roots someone hardcoded
|
|
35
|
+
* into it, while `scopeQuery` knows what is in scope at this exact site. With
|
|
36
|
+
* no query the names are colored syntactically instead — a CEL body must never
|
|
37
|
+
* read as a plain string, which is what the stock YAML grammar makes of it. */
|
|
23
38
|
export function buildSemanticTokens(
|
|
24
39
|
text: string,
|
|
25
40
|
registry: AnalysisRegistry | undefined,
|
|
26
41
|
docs?: AstDocument[],
|
|
42
|
+
analysis?: ManifestAnalysis,
|
|
27
43
|
): SemanticToken[] {
|
|
28
44
|
const astDocs = docs ?? parseToAst(text);
|
|
29
45
|
const lineOffsets = buildLineOffsets(text);
|
|
30
46
|
|
|
31
47
|
const tokens: SemanticToken[] = [];
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
//
|
|
48
|
+
const emitRange = (range: [number, number], type: SemanticToken["type"]): void => {
|
|
49
|
+
const start = offsetToPosition(range[0], lineOffsets);
|
|
50
|
+
const end = offsetToPosition(range[1], lineOffsets);
|
|
51
|
+
// Kind / capability values and CEL identifiers never span lines; a clamped
|
|
52
|
+
// single-line token.
|
|
37
53
|
if (start.line !== end.line) return;
|
|
38
|
-
tokens.push({
|
|
54
|
+
tokens.push({
|
|
55
|
+
line: start.line,
|
|
56
|
+
character: start.character,
|
|
57
|
+
length: end.character - start.character,
|
|
58
|
+
type,
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
const emit = (node: AstNode | undefined, type: SemanticToken["type"]): void => {
|
|
62
|
+
if (node) emitRange(node.range, type);
|
|
39
63
|
};
|
|
40
64
|
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
for (const doc of astDocs) {
|
|
66
|
+
if (!doc.root) continue;
|
|
67
|
+
|
|
68
|
+
// The scope is resolved per SITE. `scopeAt` caches per (resource, path) for
|
|
69
|
+
// the analysis's lifetime, which is what keeps a whole-file colourizer off
|
|
70
|
+
// the per-keystroke cost of rebuilding a context-matched environment.
|
|
71
|
+
const identity = docIdentity(doc);
|
|
72
|
+
const scopeQuery = analysis?.celScope;
|
|
73
|
+
const resource = scopeQuery?.resourceFor(identity.kind, identity.name);
|
|
74
|
+
const scopeAt = (path: string): CelScope | undefined =>
|
|
75
|
+
scopeQuery && resource ? scopeQuery.scopeAt(resource, path) : undefined;
|
|
76
|
+
|
|
77
|
+
const celTokens = (node: AstScalar, path: string): void => {
|
|
78
|
+
const segments = node.celSegments();
|
|
79
|
+
if (segments.length === 0) return;
|
|
80
|
+
const scope = scopeAt(path);
|
|
81
|
+
for (const segment of segments) {
|
|
82
|
+
for (const span of celSegmentTokens(text, segment, scope)) emitRange(span.range, span.type);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const walk = (node: AstNode, concrete: string): void => {
|
|
87
|
+
if (node.kind === "map") {
|
|
88
|
+
for (const pair of node.entries) {
|
|
89
|
+
const key = scalarString(pair.key);
|
|
90
|
+
const value = scalarString(pair.value);
|
|
91
|
+
if (key === "kind" && value && registry?.resolveDefinition(value)) {
|
|
92
|
+
emit(pair.value, "type");
|
|
93
|
+
} else if (key === "capability" && value && CAPABILITIES.has(value)) {
|
|
94
|
+
emit(pair.value, "interface");
|
|
95
|
+
}
|
|
96
|
+
if (pair.value) walk(pair.value, key != null ? joinKey(concrete, key) : concrete);
|
|
50
97
|
}
|
|
51
|
-
|
|
98
|
+
return;
|
|
52
99
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
100
|
+
if (node.kind === "seq") {
|
|
101
|
+
// Indices are kept: a CEL site's scope is addressed per item, so an
|
|
102
|
+
// index-erased path resolves the wrong context or none.
|
|
103
|
+
node.items.forEach((item, index) => walk(item, `${concrete}[${index}]`));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (node.kind === "scalar") {
|
|
107
|
+
if (node.tag === "!ref") {
|
|
108
|
+
emit(node, "variable");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
celTokens(node, concrete);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
59
114
|
|
|
60
|
-
|
|
61
|
-
if (doc.root) walk(doc.root);
|
|
115
|
+
walk(doc.root, "");
|
|
62
116
|
}
|
|
63
117
|
return tokens;
|
|
64
118
|
}
|
package/src/types.ts
CHANGED
|
@@ -54,14 +54,55 @@ export interface HoverResult {
|
|
|
54
54
|
|
|
55
55
|
/** Semantic token type names emitted by `buildSemanticTokens`. Kept to the
|
|
56
56
|
* standard VS Code / LSP set so hosts register them against a stock legend and
|
|
57
|
-
* every theme colors them without extra configuration.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
57
|
+
* every theme colors them without extra configuration.
|
|
58
|
+
*
|
|
59
|
+
* Manifest structure: `type` marks a resolved resource kind; `interface` marks
|
|
60
|
+
* a capability value; `variable` marks a `!ref` target.
|
|
61
|
+
*
|
|
62
|
+
* Inside a CEL body: `namespace` marks the ROOT of a chain, `property` a member
|
|
63
|
+
* it can resolve, `function` a call, and `number` / `string` / `keyword` /
|
|
64
|
+
* `operator` the syntax around them. A CEL name the scope CANNOT confirm gets
|
|
65
|
+
* no token — the same quiet signal an unresolved `kind:` gives, pairing with
|
|
66
|
+
* the analyzer's `CEL_UNKNOWN_FIELD`.
|
|
67
|
+
*
|
|
68
|
+
* The root is a `namespace` rather than a `variable` because colour encodes
|
|
69
|
+
* what a symbol IS, which is the invariant every language holds to — and a CEL
|
|
70
|
+
* root is not data the author declared, it is a scope the runtime injects
|
|
71
|
+
* (`request`, `steps`, `variables`, `self`). Members are uniformly `property`
|
|
72
|
+
* however deep, so a chain reads as scope · path. Colouring by the SHAPE of the
|
|
73
|
+
* value behind a name — object vs scalar — was considered and rejected: it is
|
|
74
|
+
* type-directed highlighting, so the palette becomes a type legend, a name
|
|
75
|
+
* changes colour as analysis resolves, and it says nothing exactly where the
|
|
76
|
+
* scope declares no shape. */
|
|
77
|
+
export type SemanticTokenType =
|
|
78
|
+
| "type"
|
|
79
|
+
| "interface"
|
|
80
|
+
| "variable"
|
|
81
|
+
| "property"
|
|
82
|
+
| "function"
|
|
83
|
+
| "number"
|
|
84
|
+
| "string"
|
|
85
|
+
| "keyword"
|
|
86
|
+
| "operator"
|
|
87
|
+
| "namespace";
|
|
61
88
|
|
|
62
89
|
/** The legend a host registers before mapping `buildSemanticTokens` output. The
|
|
63
|
-
* numeric token-type of each `SemanticToken` is its index in this array
|
|
64
|
-
|
|
90
|
+
* numeric token-type of each `SemanticToken` is its index in this array, and a
|
|
91
|
+
* host registers it once at activation — so new types are APPENDED, never
|
|
92
|
+
* inserted, or an already-registered legend would repaint every existing
|
|
93
|
+
* token as something else. */
|
|
94
|
+
export const SEMANTIC_TOKEN_LEGEND: readonly SemanticTokenType[] = [
|
|
95
|
+
"type",
|
|
96
|
+
"interface",
|
|
97
|
+
"variable",
|
|
98
|
+
"property",
|
|
99
|
+
"function",
|
|
100
|
+
"number",
|
|
101
|
+
"string",
|
|
102
|
+
"keyword",
|
|
103
|
+
"operator",
|
|
104
|
+
"namespace",
|
|
105
|
+
];
|
|
65
106
|
|
|
66
107
|
/** One absolute-positioned semantic token. Every Telo semantic token is
|
|
67
108
|
* single-line (kinds and capabilities never wrap), so a `{line, char, length}`
|