@telorun/ide-support 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cel-chain.d.ts +36 -0
  2. package/dist/cel-chain.d.ts.map +1 -0
  3. package/dist/cel-chain.js +77 -0
  4. package/dist/completions/detect-context.d.ts +5 -5
  5. package/dist/completions/detect-context.d.ts.map +1 -1
  6. package/dist/completions/detect-context.js +72 -5
  7. package/dist/completions/prop-keys.d.ts +1 -1
  8. package/dist/completions/prop-keys.d.ts.map +1 -1
  9. package/dist/completions/prop-keys.js +18 -1
  10. package/dist/definition/resolve-cel-target.d.ts.map +1 -1
  11. package/dist/definition/resolve-cel-target.js +1 -60
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -0
  15. package/dist/rename/build-rename.d.ts +39 -0
  16. package/dist/rename/build-rename.d.ts.map +1 -0
  17. package/dist/rename/build-rename.js +407 -0
  18. package/dist/rename/find-sites.d.ts +49 -0
  19. package/dist/rename/find-sites.d.ts.map +1 -0
  20. package/dist/rename/find-sites.js +202 -0
  21. package/dist/rename/index.d.ts +3 -0
  22. package/dist/rename/index.d.ts.map +1 -0
  23. package/dist/rename/index.js +1 -0
  24. package/dist/rename/types.d.ts +55 -0
  25. package/dist/rename/types.d.ts.map +1 -0
  26. package/dist/rename/types.js +1 -0
  27. package/package.json +2 -2
  28. package/src/cel-chain.ts +86 -0
  29. package/src/completions/detect-context.ts +73 -6
  30. package/src/completions/prop-keys.ts +23 -2
  31. package/src/definition/resolve-cel-target.ts +1 -68
  32. package/src/index.ts +1 -0
  33. package/src/rename/build-rename.ts +513 -0
  34. package/src/rename/find-sites.ts +215 -0
  35. package/src/rename/index.ts +9 -0
  36. package/src/rename/types.ts +51 -0
@@ -0,0 +1,215 @@
1
+ import {
2
+ CelParseError,
3
+ type AstDocument,
4
+ type AstNode,
5
+ type AstScalar,
6
+ type CelNode,
7
+ } from "@telorun/analyzer";
8
+
9
+ import { walkCel } from "../cel-chain.js";
10
+ import { scalarString } from "../completions/resolve-node.js";
11
+
12
+ /**
13
+ * Every place a name is written, found over the read-only YAML AST plus the CEL
14
+ * AST inside each scalar. Document offsets in, document offsets out — the
15
+ * caller maps them to `Range`s once, with the line table it already built.
16
+ *
17
+ * **Offsets rather than ranges, and a flat list rather than a tree**, because
18
+ * the two things a rename must guarantee are that no site is missed and that no
19
+ * two edits overlap. Both are properties of a flat, sorted offset list, and
20
+ * neither is checkable once the sites have been shaped per-feature.
21
+ *
22
+ * A `!ref` scalar's own range is its VALUE, excluding the tag (`!ref other` →
23
+ * the span of `other`), so a local reference is a whole-node replacement. A CEL
24
+ * identifier is a sub-span of its scalar, taken from `propertyRange` — which the
25
+ * analyzer's `CelNode` has carried since it was written, for exactly this.
26
+ */
27
+ export interface NameSite {
28
+ /** `[start, end]` in document offsets — the identifier alone, never the
29
+ * enclosing scalar or the `!ref`/`!cel` tag. */
30
+ range: [number, number];
31
+ }
32
+
33
+ /** Walk every scalar of a document, in source order. */
34
+ function eachScalar(node: AstNode, visit: (scalar: AstScalar) => void): void {
35
+ if (node.kind === "map") {
36
+ for (const pair of node.entries) {
37
+ eachScalar(pair.key, visit);
38
+ if (pair.value) eachScalar(pair.value, visit);
39
+ }
40
+ return;
41
+ }
42
+ if (node.kind === "seq") {
43
+ for (const item of node.items) eachScalar(item, visit);
44
+ return;
45
+ }
46
+ visit(node);
47
+ }
48
+
49
+ /** Every CEL node of a scalar's every segment.
50
+ *
51
+ * A body that does not parse yields nothing: an author mid-edit is not a
52
+ * reason to fail a rename, and the analyzer reports the syntax error itself.
53
+ * Only that failure is tolerated — a defect in the CEL wrapper propagates,
54
+ * the posture `resolveCelTarget` already takes. */
55
+ function eachCelNode(scalar: AstScalar, visit: (node: CelNode) => void): void {
56
+ for (const segment of scalar.celSegments()) {
57
+ let ast: CelNode;
58
+ try {
59
+ ast = segment.ast();
60
+ } catch (error) {
61
+ if (!(error instanceof CelParseError)) throw error;
62
+ continue;
63
+ }
64
+ walkCel(ast, visit);
65
+ }
66
+ }
67
+
68
+ /** `<scope>.<name>` read as a member access — `resources.db`, `steps.build`,
69
+ * `variables.apiUrl`. Returns the span of `name` alone. */
70
+ function scopeMemberSite(node: CelNode, scope: string, name: string): NameSite | undefined {
71
+ if (node.kind !== "member" || node.property !== name) return undefined;
72
+ if (node.target.kind !== "ident" || node.target.name !== scope) return undefined;
73
+ return { range: node.propertyRange };
74
+ }
75
+
76
+ const SELF_PREFIX = "Self.";
77
+
78
+ /** The span of `name` inside a `!ref` scalar, or undefined when the scalar names
79
+ * something else. Accepts the bare form and the `Self.`-qualified one, which
80
+ * also resolves locally; `<Alias>.<name>` is a different module's export and is
81
+ * deliberately not matched. */
82
+ function refSite(scalar: AstScalar, name: string): NameSite | undefined {
83
+ if (scalar.tag !== "!ref") return undefined;
84
+ const [start, end] = scalar.range;
85
+ const raw = refText(scalar);
86
+ if (raw === name) return { range: [start, end] };
87
+ if (raw === `${SELF_PREFIX}${name}`) return { range: [start + SELF_PREFIX.length, end] };
88
+ return undefined;
89
+ }
90
+
91
+ /** A `!ref` scalar's resolved value is a `TaggedSentinel`, so the written text
92
+ * is read off the sentinel rather than assumed to be a plain string. */
93
+ function refText(scalar: AstScalar): string | undefined {
94
+ const value = scalar.value as { source?: unknown } | string | undefined;
95
+ if (typeof value === "string") return value;
96
+ if (value && typeof value === "object" && typeof value.source === "string") return value.source;
97
+ return undefined;
98
+ }
99
+
100
+ /** A resource's references within one document: `!ref <name>` (and its `Self.`
101
+ * form) plus `resources.<name>` in CEL. */
102
+ export function resourceSites(doc: AstDocument, name: string): NameSite[] {
103
+ const sites: NameSite[] = [];
104
+ if (!doc.root) return sites;
105
+ eachScalar(doc.root, (scalar) => {
106
+ const ref = refSite(scalar, name);
107
+ if (ref) sites.push(ref);
108
+ eachCelNode(scalar, (node) => {
109
+ const site = scopeMemberSite(node, "resources", name);
110
+ if (site) sites.push(site);
111
+ });
112
+ });
113
+ return sites;
114
+ }
115
+
116
+ /** A step's references within its declaring document: `steps.<name>` in CEL.
117
+ * Deliberately document-scoped — `steps.<name>.result` is readable only inside
118
+ * the resource whose body declares the step, and a resource is one document. */
119
+ export function stepSites(doc: AstDocument, name: string): NameSite[] {
120
+ const sites: NameSite[] = [];
121
+ if (!doc.root) return sites;
122
+ eachScalar(doc.root, (scalar) => {
123
+ eachCelNode(scalar, (node) => {
124
+ const site = scopeMemberSite(node, "steps", name);
125
+ if (site) sites.push(site);
126
+ });
127
+ });
128
+ return sites;
129
+ }
130
+
131
+ /** A `variables:` / `secrets:` / `ports:` entry's reads: `<block>.<name>`. */
132
+ export function declarationSites(doc: AstDocument, block: string, name: string): NameSite[] {
133
+ const sites: NameSite[] = [];
134
+ if (!doc.root) return sites;
135
+ eachScalar(doc.root, (scalar) => {
136
+ eachCelNode(scalar, (node) => {
137
+ const site = scopeMemberSite(node, block, name);
138
+ if (site) sites.push(site);
139
+ });
140
+ });
141
+ return sites;
142
+ }
143
+
144
+ /**
145
+ * Every map in a document that declares a resource named `name` — a `kind:`
146
+ * beside a `metadata.name`.
147
+ *
148
+ * Used to detect a **shadowing scope declaration**: a resource declared inside
149
+ * another's `x-telo-scope` array shadows a module-level name of the same
150
+ * spelling within that scope's regions, so renaming the module-level one must
151
+ * not rewrite references that resolve to the scoped one. Detected structurally
152
+ * rather than by reading `x-telo-scope` off the kind's schema, because the
153
+ * question a rename needs answered is "is this spelling declared more than once
154
+ * in reach", which is true of any nested declaration whether or not the slot
155
+ * carrying it is annotated.
156
+ */
157
+ export function resourceDeclarations(doc: AstDocument, name: string): Array<[number, number]> {
158
+ const found: Array<[number, number]> = [];
159
+ const visit = (node: AstNode): void => {
160
+ if (node.kind === "map") {
161
+ let hasKind = false;
162
+ let nameNode: AstScalar | undefined;
163
+ for (const pair of node.entries) {
164
+ const key = scalarString(pair.key);
165
+ if (key === "kind") hasKind = true;
166
+ if (key === "metadata" && pair.value?.kind === "map") {
167
+ for (const inner of pair.value.entries) {
168
+ if (scalarString(inner.key) === "name" && inner.value?.kind === "scalar") {
169
+ nameNode = inner.value;
170
+ }
171
+ }
172
+ }
173
+ }
174
+ if (hasKind && nameNode && scalarString(nameNode) === name) found.push(nameNode.range);
175
+ for (const pair of node.entries) if (pair.value) visit(pair.value);
176
+ return;
177
+ }
178
+ if (node.kind === "seq") {
179
+ for (const item of node.items) visit(item);
180
+ }
181
+ };
182
+ if (doc.root) visit(doc.root);
183
+ return found;
184
+ }
185
+
186
+ /** Every step in a document declaring `name:` — the span of the name scalar.
187
+ * More than one means the spelling is ambiguous within the resource, which is
188
+ * a refusal rather than a guess. */
189
+ export function stepDeclarations(doc: AstDocument, name: string): Array<[number, number]> {
190
+ const found: Array<[number, number]> = [];
191
+ const visit = (node: AstNode, inStepArray: boolean): void => {
192
+ if (node.kind === "map") {
193
+ // A step is a map in a sequence carrying a `name:`; a resource's own
194
+ // `metadata.name` is nested under `metadata:` and so never matches here.
195
+ if (inStepArray) {
196
+ for (const pair of node.entries) {
197
+ if (
198
+ scalarString(pair.key) === "name" &&
199
+ pair.value?.kind === "scalar" &&
200
+ scalarString(pair.value) === name
201
+ ) {
202
+ found.push(pair.value.range);
203
+ }
204
+ }
205
+ }
206
+ for (const pair of node.entries) if (pair.value) visit(pair.value, false);
207
+ return;
208
+ }
209
+ if (node.kind === "seq") {
210
+ for (const item of node.items) visit(item, true);
211
+ }
212
+ };
213
+ if (doc.root) visit(doc.root, false);
214
+ return found;
215
+ }
@@ -0,0 +1,9 @@
1
+ export { buildRename, prepareRename } from "./build-rename.js";
2
+ export type {
3
+ RenameEdit,
4
+ RenameFileEdits,
5
+ RenamePreparation,
6
+ RenameResult,
7
+ RenameSymbol,
8
+ RenameSymbolKind,
9
+ } from "./types.js";
@@ -0,0 +1,51 @@
1
+ import type { Range } from "../types.js";
2
+
3
+ /**
4
+ * Which naming surface a rename targets. Only the value-level, module-local
5
+ * ones are supported — see `build-rename.ts` for why each of the others is a
6
+ * refusal rather than an omission.
7
+ */
8
+ export type RenameSymbolKind =
9
+ /** A resource instance's `metadata.name`. */
10
+ | "resource"
11
+ /** A `name:` on a step inside a step array. */
12
+ | "step"
13
+ /** A key of the module doc's `variables:` / `secrets:` / `ports:` block. */
14
+ | "declaration";
15
+
16
+ /** What the cursor resolved to, and the span the host pre-fills in its rename
17
+ * box. `block` is set only for a `declaration`. */
18
+ export interface RenameSymbol {
19
+ kind: RenameSymbolKind;
20
+ name: string;
21
+ range: Range;
22
+ block?: "variables" | "secrets" | "ports";
23
+ }
24
+
25
+ /** One replacement. `range` is a source span in `uri`; `newText` replaces it
26
+ * wholesale. Spans never overlap and never cross a line, because every site is
27
+ * either a bare scalar value or an identifier inside one. */
28
+ export interface RenameEdit {
29
+ range: Range;
30
+ newText: string;
31
+ }
32
+
33
+ export interface RenameFileEdits {
34
+ uri: string;
35
+ edits: RenameEdit[];
36
+ }
37
+
38
+ /**
39
+ * A refusal carries the reason, always. A rename the tool declines is a
40
+ * decision the author has to act on — silently returning "nothing to rename"
41
+ * would read as "this name has no references", which is the opposite of what a
42
+ * refusal usually means here (an exported name has too many, in files this
43
+ * workspace cannot see).
44
+ */
45
+ export type RenamePreparation =
46
+ | { ok: true; symbol: RenameSymbol }
47
+ | { ok: false; reason: string };
48
+
49
+ export type RenameResult =
50
+ | { ok: true; symbol: RenameSymbol; files: RenameFileEdits[] }
51
+ | { ok: false; reason: string };