@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,55 @@
1
+ import type { Range } from "../types.js";
2
+ /**
3
+ * Which naming surface a rename targets. Only the value-level, module-local
4
+ * ones are supported — see `build-rename.ts` for why each of the others is a
5
+ * refusal rather than an omission.
6
+ */
7
+ export type RenameSymbolKind =
8
+ /** A resource instance's `metadata.name`. */
9
+ "resource"
10
+ /** A `name:` on a step inside a step array. */
11
+ | "step"
12
+ /** A key of the module doc's `variables:` / `secrets:` / `ports:` block. */
13
+ | "declaration";
14
+ /** What the cursor resolved to, and the span the host pre-fills in its rename
15
+ * box. `block` is set only for a `declaration`. */
16
+ export interface RenameSymbol {
17
+ kind: RenameSymbolKind;
18
+ name: string;
19
+ range: Range;
20
+ block?: "variables" | "secrets" | "ports";
21
+ }
22
+ /** One replacement. `range` is a source span in `uri`; `newText` replaces it
23
+ * wholesale. Spans never overlap and never cross a line, because every site is
24
+ * either a bare scalar value or an identifier inside one. */
25
+ export interface RenameEdit {
26
+ range: Range;
27
+ newText: string;
28
+ }
29
+ export interface RenameFileEdits {
30
+ uri: string;
31
+ edits: RenameEdit[];
32
+ }
33
+ /**
34
+ * A refusal carries the reason, always. A rename the tool declines is a
35
+ * decision the author has to act on — silently returning "nothing to rename"
36
+ * would read as "this name has no references", which is the opposite of what a
37
+ * refusal usually means here (an exported name has too many, in files this
38
+ * workspace cannot see).
39
+ */
40
+ export type RenamePreparation = {
41
+ ok: true;
42
+ symbol: RenameSymbol;
43
+ } | {
44
+ ok: false;
45
+ reason: string;
46
+ };
47
+ export type RenameResult = {
48
+ ok: true;
49
+ symbol: RenameSymbol;
50
+ files: RenameFileEdits[];
51
+ } | {
52
+ ok: false;
53
+ reason: string;
54
+ };
55
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/rename/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;GAIG;AACH,MAAM,MAAM,gBAAgB;AAC1B,6CAA6C;AAC3C,UAAU;AACZ,+CAA+C;GAC7C,MAAM;AACR,4EAA4E;GAC1E,aAAa,CAAC;AAElB;oDACoD;AACpD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,CAAC,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;CAC3C;AAED;;8DAE8D;AAC9D,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,eAAe,EAAE,CAAA;CAAE,GAC5D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/ide-support",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "Editor-host-agnostic IDE support (completions, diagnostic normalization) for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -36,7 +36,7 @@
36
36
  "src/**"
37
37
  ],
38
38
  "dependencies": {
39
- "@telorun/analyzer": "0.60.0"
39
+ "@telorun/analyzer": "0.62.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^20.0.0",
@@ -0,0 +1,86 @@
1
+ import type { CelNode } from "@telorun/analyzer";
2
+
3
+ /** One identifier of a dotted CEL chain, with the span a cursor hit-tests
4
+ * against and a rename replaces. */
5
+ export interface ChainPart {
6
+ name: string;
7
+ range: [number, number];
8
+ }
9
+
10
+ /**
11
+ * Flatten `a.b.c` into its identifiers. Returns undefined as soon as the chain
12
+ * is rooted in something other than a plain identifier (a call, an index), so a
13
+ * navigable — or renameable — prefix is never invented out of a computed
14
+ * expression.
15
+ */
16
+ export function flattenChain(node: CelNode): ChainPart[] | undefined {
17
+ if (node.kind === "ident") return [{ name: node.name, range: node.range }];
18
+ if (node.kind !== "member") return undefined;
19
+ const head = flattenChain(node.target);
20
+ return head ? [...head, { name: node.property, range: node.propertyRange }] : undefined;
21
+ }
22
+
23
+ /**
24
+ * A node's children.
25
+ *
26
+ * Exhaustive by construction: a new `CelNode` variant fails the build here
27
+ * rather than silently going unwalked, so the analyzer's node model and the
28
+ * walks over it cannot drift apart unnoticed. That matters twice over — for
29
+ * go-to-definition an unwalked variant is a missed jump, for a rename it is a
30
+ * reference left pointing at the old name.
31
+ */
32
+ export function celChildren(node: CelNode): CelNode[] {
33
+ switch (node.kind) {
34
+ case "literal":
35
+ case "ident":
36
+ return [];
37
+ case "member":
38
+ return [node.target];
39
+ case "index":
40
+ return [node.target, node.index];
41
+ case "call":
42
+ return node.args;
43
+ case "methodCall":
44
+ return [node.receiver, ...node.args];
45
+ case "list":
46
+ return node.items;
47
+ case "map":
48
+ return node.entries.flatMap((e) => [e.key, e.value]);
49
+ case "ternary":
50
+ return [node.cond, node.then, node.else];
51
+ case "unary":
52
+ return [node.operand];
53
+ case "binary":
54
+ return [node.left, node.right];
55
+ }
56
+ const unhandled: never = node;
57
+ throw new Error(`Unhandled CEL node: ${JSON.stringify(unhandled)}`);
58
+ }
59
+
60
+ /** Every node of the tree, outermost first. */
61
+ export function walkCel(node: CelNode, visit: (node: CelNode) => void): void {
62
+ visit(node);
63
+ for (const child of celChildren(node)) walkCel(child, visit);
64
+ }
65
+
66
+ /**
67
+ * The dotted chain under `offset`, and which of its identifiers was hit. The
68
+ * walk is outermost-first so the longest chain wins — `resources.store.conn`
69
+ * resolves as one chain rather than as its `resources.store` prefix.
70
+ */
71
+ export function chainAt(
72
+ node: CelNode,
73
+ offset: number,
74
+ ): { parts: ChainPart[]; index: number } | undefined {
75
+ if (offset < node.range[0] || offset > node.range[1]) return undefined;
76
+ const parts = flattenChain(node);
77
+ if (parts) {
78
+ const index = parts.findIndex((p) => offset >= p.range[0] && offset <= p.range[1]);
79
+ if (index >= 0) return { parts, index };
80
+ }
81
+ for (const child of celChildren(node)) {
82
+ const hit = chainAt(child, offset);
83
+ if (hit) return hit;
84
+ }
85
+ return undefined;
86
+ }
@@ -66,18 +66,70 @@ function peelCombinators(node: Record<string, any>): Record<string, any>[] {
66
66
  return out;
67
67
  }
68
68
 
69
+ /** One JSON Pointer segment: RFC 6901 escapes, then percent-decoding, which a
70
+ * pointer carried in a URI fragment is subject to. A malformed escape is
71
+ * returned raw rather than thrown — `decodeURIComponent` raises `URIError`, and
72
+ * a stray `%` in someone's `$defs` key must not take completion down. */
73
+ function decodePointerSegment(segment: string): string {
74
+ const unescaped = segment.replace(/~1/g, "/").replace(/~0/g, "~");
75
+ try {
76
+ return decodeURIComponent(unescaped);
77
+ } catch {
78
+ return unescaped;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Follow a document-local `$ref` (`#/$defs/<Name>`) against the schema root.
84
+ *
85
+ * The one reference form that occurs inside a kind schema, and the one the
86
+ * editor's resolver accepts — a schema-valued slot points at the hoisted
87
+ * `JsonSchema7` / `KindSchema` fragment this way, and the fragment points at
88
+ * itself to describe a nested schema. Without the hop, completion stopped dead
89
+ * at the first key of every `schema:` / `status:` block.
90
+ *
91
+ * A chain ends at a REPEATED pointer, which is what bounds the walk: a
92
+ * self-referential fragment is the normal case here, so a cycle must degrade to
93
+ * "no completion" rather than hang the editor.
94
+ */
95
+ function resolveLocalRef(
96
+ node: Record<string, any> | undefined,
97
+ root: Record<string, any>,
98
+ ): Record<string, any> | undefined {
99
+ let current = node;
100
+ const seen = new Set<string>();
101
+ while (current && typeof current.$ref === "string" && current.$ref.startsWith("#/")) {
102
+ const pointer = current.$ref;
103
+ if (seen.has(pointer)) return undefined;
104
+ seen.add(pointer);
105
+ let target: any = root;
106
+ for (const segment of pointer.slice(2).split("/")) {
107
+ target = target?.[decodePointerSegment(segment)];
108
+ }
109
+ if (!target || typeof target !== "object") return undefined;
110
+ // Keep whatever the slot declared beside the `$ref` (its title, its
111
+ // `x-telo-fragment` stamp) — that is what tells a consumer WHICH shape it
112
+ // pointed at, and draft-07 drops it at the validation layer only.
113
+ const { $ref: _, ...siblings } = current;
114
+ current = { ...target, ...siblings };
115
+ }
116
+ return current;
117
+ }
118
+
69
119
  /** Navigate a JSON Schema hierarchy following `path`, auto-descending into
70
- * array items and peeling `anyOf` / `oneOf` branches. When multiple peeled
71
- * branches define `properties`, returns a synthetic node whose `properties`
72
- * is the union (first-wins on key collision) and whose `required` is the
73
- * intersection — enough for propKeyCompletions to surface every key a value
74
- * at this slot can legally carry. */
120
+ * array items, peeling `anyOf` / `oneOf` branches and following document-local
121
+ * `$ref`s. When multiple peeled branches define `properties`, returns a
122
+ * synthetic node whose `properties` is the union (first-wins on key collision)
123
+ * and whose `required` is the intersection — enough for propKeyCompletions to
124
+ * surface every key a value at this slot can legally carry. */
75
125
  export function navigateSchema(
76
126
  schema: Record<string, any>,
77
127
  path: string[],
78
128
  ): Record<string, any> | undefined {
79
- let current: Record<string, any> = schema;
129
+ let current: Record<string, any> | undefined = schema;
80
130
  for (const segment of path) {
131
+ current = resolveLocalRef(current, schema);
132
+ if (!current) return undefined;
81
133
  const candidates = peelCombinators(current).flatMap((node) => {
82
134
  const expanded: Record<string, any>[] = [];
83
135
  let cur: Record<string, any> = node;
@@ -93,9 +145,24 @@ export function navigateSchema(
93
145
  break;
94
146
  }
95
147
  }
148
+ // A map-valued node (a schema's `properties:`, a kind's name-keyed field)
149
+ // names its entries nowhere, so every segment lands on
150
+ // `additionalProperties`. Without this the walk stopped one level into a
151
+ // `schema:` block — at exactly the field the author is writing.
152
+ if (!next) {
153
+ for (const cand of candidates) {
154
+ const additional = cand.additionalProperties;
155
+ if (additional && typeof additional === "object") {
156
+ next = additional as Record<string, any>;
157
+ break;
158
+ }
159
+ }
160
+ }
96
161
  if (!next) return undefined;
97
162
  current = next;
98
163
  }
164
+ current = resolveLocalRef(current, schema);
165
+ if (!current) return undefined;
99
166
  // Auto-descend through a trailing array at the leaf (e.g. cursor inside `mounts:` items)
100
167
  while (current.type === "array" && current.items) {
101
168
  current = current.items as Record<string, any>;
@@ -1,4 +1,8 @@
1
- import type { AnalysisRegistry } from "@telorun/analyzer";
1
+ import {
2
+ manifestFragmentOf,
3
+ TELO_SCHEMA_ANNOTATIONS,
4
+ type AnalysisRegistry,
5
+ } from "@telorun/analyzer";
2
6
  import type { CompletionResult } from "../types.js";
3
7
  import { navigateSchema } from "./detect-context.js";
4
8
 
@@ -52,11 +56,28 @@ export function propKeyCompletions(
52
56
  const properties =
53
57
  yamlPath.length === 0
54
58
  ? { ...ROOT_IMPLICIT_PROPS, ...(targetSchema.properties as Record<string, any>) }
55
- : (targetSchema.properties as Record<string, any>);
59
+ : { ...(targetSchema.properties as Record<string, any>), ...annotationKeys(targetSchema) };
56
60
 
57
61
  return buildItems(properties, existingKeys, required);
58
62
  }
59
63
 
64
+ /**
65
+ * The `x-telo-*` vocabulary, offered only inside a KIND's own schema.
66
+ *
67
+ * Read off the fragment stamp rather than the position: a kind schema nests, so
68
+ * "am I inside one" is a fact about the node, not about how deep the cursor
69
+ * sits. A plain data schema — an `inputType:`, a `status:` block — stamps
70
+ * `JsonSchema7` and gets nothing, because an annotation there configures a slot
71
+ * that does not exist.
72
+ *
73
+ * The vocabulary lives on the analyzer side and never enters a manifest: a
74
+ * `properties` map holding a literal `x-telo-ref` key reads to the annotation
75
+ * walkers as an annotated node.
76
+ */
77
+ function annotationKeys(node: Record<string, any>): Record<string, any> {
78
+ return manifestFragmentOf(node) === "KindSchema" ? TELO_SCHEMA_ANNOTATIONS : {};
79
+ }
80
+
60
81
  function buildItems(
61
82
  properties: Record<string, any>,
62
83
  existingKeys: Set<string>,
@@ -13,79 +13,12 @@ import {
13
13
  moduleFiles,
14
14
  } from "./manifest-navigation.js";
15
15
  import { resolveExportedResource } from "./resolve-export-chain.js";
16
+ import { chainAt, type ChainPart } from "../cel-chain.js";
16
17
 
17
18
  /** Root CEL scopes whose members are declared as a block on the module doc, so
18
19
  * `variables.port` navigates to `variables:` and then to its `port:` entry. */
19
20
  const DECLARATION_SCOPES = new Set(["variables", "secrets", "ports"]);
20
21
 
21
- /** One identifier of a dotted CEL chain, with the span the cursor hit-tests
22
- * against. */
23
- interface ChainPart {
24
- name: string;
25
- range: [number, number];
26
- }
27
-
28
- /** Flatten `a.b.c` into its identifiers. Returns undefined as soon as the chain
29
- * is rooted in something other than a plain identifier (a call, an index), so a
30
- * navigable prefix is never invented out of a computed expression. */
31
- function flattenChain(node: CelNode): ChainPart[] | undefined {
32
- if (node.kind === "ident") return [{ name: node.name, range: node.range }];
33
- if (node.kind !== "member") return undefined;
34
- const head = flattenChain(node.target);
35
- return head ? [...head, { name: node.property, range: node.propertyRange }] : undefined;
36
- }
37
-
38
- /** Exhaustive by construction: a new `CelNode` variant fails the build here
39
- * rather than silently going unwalked, so the analyzer's node model and this
40
- * walk cannot drift apart unnoticed. */
41
- function celChildren(node: CelNode): CelNode[] {
42
- switch (node.kind) {
43
- case "literal":
44
- case "ident":
45
- return [];
46
- case "member":
47
- return [node.target];
48
- case "index":
49
- return [node.target, node.index];
50
- case "call":
51
- return node.args;
52
- case "methodCall":
53
- return [node.receiver, ...node.args];
54
- case "list":
55
- return node.items;
56
- case "map":
57
- return node.entries.flatMap((e) => [e.key, e.value]);
58
- case "ternary":
59
- return [node.cond, node.then, node.else];
60
- case "unary":
61
- return [node.operand];
62
- case "binary":
63
- return [node.left, node.right];
64
- }
65
- const unhandled: never = node;
66
- throw new Error(`Unhandled CEL node: ${JSON.stringify(unhandled)}`);
67
- }
68
-
69
- /** The dotted chain under `offset`, and which of its identifiers was hit. The
70
- * walk is outermost-first so the longest chain wins — `resources.Store.conn`
71
- * resolves as one chain rather than as its `resources.Store` prefix. */
72
- function chainAt(
73
- node: CelNode,
74
- offset: number,
75
- ): { parts: ChainPart[]; index: number } | undefined {
76
- if (offset < node.range[0] || offset > node.range[1]) return undefined;
77
- const parts = flattenChain(node);
78
- if (parts) {
79
- const index = parts.findIndex((p) => offset >= p.range[0] && offset <= p.range[1]);
80
- if (index >= 0) return { parts, index };
81
- }
82
- for (const child of celChildren(node)) {
83
- const hit = chainAt(child, offset);
84
- if (hit) return hit;
85
- }
86
- return undefined;
87
- }
88
-
89
22
  /** `resources.<name>` is a local instance; `resources.<Alias>.<name>` is an
90
23
  * imported module's exported one. A local name wins over an import alias, so a
91
24
  * deeper access on a local instance (`resources.db.url`) reads as a field of
package/src/index.ts CHANGED
@@ -4,4 +4,5 @@ export * from "./diagnostics/index.js";
4
4
  export * from "./hover/index.js";
5
5
  export * from "./semantic-tokens/index.js";
6
6
  export * from "./definition/index.js";
7
+ export * from "./rename/index.js";
7
8
  export * from "./import-upgrades/index.js";