@telorun/ide-support 0.6.0 → 0.7.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.
Files changed (53) hide show
  1. package/dist/completions/build.d.ts +2 -2
  2. package/dist/completions/build.d.ts.map +1 -1
  3. package/dist/completions/build.js +39 -64
  4. package/dist/completions/detect-context.d.ts +16 -43
  5. package/dist/completions/detect-context.d.ts.map +1 -1
  6. package/dist/completions/detect-context.js +63 -262
  7. package/dist/completions/import-source.d.ts +5 -5
  8. package/dist/completions/import-source.d.ts.map +1 -1
  9. package/dist/completions/import-source.js +15 -15
  10. package/dist/completions/resolve-node.d.ts +50 -0
  11. package/dist/completions/resolve-node.d.ts.map +1 -0
  12. package/dist/completions/resolve-node.js +269 -0
  13. package/dist/completions/valid-capabilities.d.ts +3 -0
  14. package/dist/completions/valid-capabilities.d.ts.map +1 -1
  15. package/dist/completions/valid-capabilities.js +10 -0
  16. package/dist/definition/build-definition.d.ts +13 -0
  17. package/dist/definition/build-definition.d.ts.map +1 -0
  18. package/dist/definition/build-definition.js +98 -0
  19. package/dist/definition/index.d.ts +2 -0
  20. package/dist/definition/index.d.ts.map +1 -0
  21. package/dist/definition/index.js +1 -0
  22. package/dist/hover/build-hover.d.ts +4 -0
  23. package/dist/hover/build-hover.d.ts.map +1 -0
  24. package/dist/hover/build-hover.js +125 -0
  25. package/dist/hover/index.d.ts +2 -0
  26. package/dist/hover/index.d.ts.map +1 -0
  27. package/dist/hover/index.js +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -0
  31. package/dist/semantic-tokens/build-semantic-tokens.d.ts +12 -0
  32. package/dist/semantic-tokens/build-semantic-tokens.d.ts.map +1 -0
  33. package/dist/semantic-tokens/build-semantic-tokens.js +55 -0
  34. package/dist/semantic-tokens/index.d.ts +2 -0
  35. package/dist/semantic-tokens/index.d.ts.map +1 -0
  36. package/dist/semantic-tokens/index.js +1 -0
  37. package/dist/types.d.ts +45 -6
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/types.js +3 -0
  40. package/package.json +2 -2
  41. package/src/completions/build.ts +40 -72
  42. package/src/completions/detect-context.ts +74 -291
  43. package/src/completions/import-source.ts +16 -16
  44. package/src/completions/resolve-node.ts +405 -0
  45. package/src/completions/valid-capabilities.ts +11 -0
  46. package/src/definition/build-definition.ts +123 -0
  47. package/src/definition/index.ts +1 -0
  48. package/src/hover/build-hover.ts +149 -0
  49. package/src/hover/index.ts +1 -0
  50. package/src/index.ts +3 -0
  51. package/src/semantic-tokens/build-semantic-tokens.ts +64 -0
  52. package/src/semantic-tokens/index.ts +1 -0
  53. package/src/types.ts +57 -6
@@ -1,4 +1,4 @@
1
- import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
1
+ import type { CompletionResult, IdeEnvironmentAdapter, ReplaceRange } from "../types.js";
2
2
  /**
3
3
  * Completions for an import source — an `imports:` map entry on a module doc
4
4
  * (either the scalar shorthand `Alias: <src>` or the `source:` under the
@@ -15,9 +15,9 @@ import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
15
15
  * search below, whose query is the whole typed prefix — so the hub fuzzy-matches
16
16
  * `oci://ghcr.io/aws/telo-s3` as readily as a bare `s3`.
17
17
  *
18
- * `valueStartColumn` is forwarded onto every result so the host can replace
19
- * the whole typed value, not just the trailing word (Monaco / VSCode word
20
- * boundaries don't cross `/` or `@`).
18
+ * `replaceRange` is forwarded onto every result so the host replaces the whole
19
+ * value, not just the trailing word (Monaco / VSCode word boundaries don't
20
+ * cross `/` or `@`).
21
21
  */
22
- export declare function importSourceCompletions(prefix: string, valueStartColumn: number, adapter: IdeEnvironmentAdapter | undefined): Promise<CompletionResult[]>;
22
+ export declare function importSourceCompletions(prefix: string, replaceRange: ReplaceRange, adapter: IdeEnvironmentAdapter | undefined): Promise<CompletionResult[]>;
23
23
  //# sourceMappingURL=import-source.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"import-source.d.ts","sourceRoot":"","sources":["../../src/completions/import-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAY3E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,qBAAqB,GAAG,SAAS,GACzC,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyB7B"}
1
+ {"version":3,"file":"import-source.d.ts","sourceRoot":"","sources":["../../src/completions/import-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAYzF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,qBAAqB,GAAG,SAAS,GACzC,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyB7B"}
@@ -22,11 +22,11 @@ const PATH_PROBE_LIMIT = 50;
22
22
  * search below, whose query is the whole typed prefix — so the hub fuzzy-matches
23
23
  * `oci://ghcr.io/aws/telo-s3` as readily as a bare `s3`.
24
24
  *
25
- * `valueStartColumn` is forwarded onto every result so the host can replace
26
- * the whole typed value, not just the trailing word (Monaco / VSCode word
27
- * boundaries don't cross `/` or `@`).
25
+ * `replaceRange` is forwarded onto every result so the host replaces the whole
26
+ * value, not just the trailing word (Monaco / VSCode word boundaries don't
27
+ * cross `/` or `@`).
28
28
  */
29
- export async function importSourceCompletions(prefix, valueStartColumn, adapter) {
29
+ export async function importSourceCompletions(prefix, replaceRange, adapter) {
30
30
  if (!adapter)
31
31
  return [];
32
32
  if (prefix.startsWith("http://") ||
@@ -36,17 +36,17 @@ export async function importSourceCompletions(prefix, valueStartColumn, adapter)
36
36
  }
37
37
  const isRelativeShape = prefix === "" || prefix.startsWith(".") || prefix.startsWith("/");
38
38
  if (isRelativeShape) {
39
- return relativePathCompletions(prefix, valueStartColumn, adapter);
39
+ return relativePathCompletions(prefix, replaceRange, adapter);
40
40
  }
41
41
  // The version (or `@sha256:` digest) is the trailing `@`-segment, so split on
42
42
  // the LAST `@` — a digest-pinned ref keeps everything before it as the ref.
43
43
  const atIdx = prefix.lastIndexOf("@");
44
44
  if (atIdx > 0) {
45
- return versionCompletions(prefix, atIdx, valueStartColumn, adapter);
45
+ return versionCompletions(prefix, atIdx, replaceRange, adapter);
46
46
  }
47
- return refSearchCompletions(prefix, valueStartColumn, adapter);
47
+ return refSearchCompletions(prefix, replaceRange, adapter);
48
48
  }
49
- async function relativePathCompletions(prefix, valueStartColumn, adapter) {
49
+ async function relativePathCompletions(prefix, replaceRange, adapter) {
50
50
  // Empty prefix → seed `./` and `../` so the user gets traction; otherwise
51
51
  // we'd return an unfiltered dump of the manifest directory which is rarely
52
52
  // what the user wants for an import source.
@@ -57,14 +57,14 @@ async function relativePathCompletions(prefix, valueStartColumn, adapter) {
57
57
  kind: "folder",
58
58
  insertText: "./",
59
59
  sortText: "0_./",
60
- replaceFromColumn: valueStartColumn,
60
+ replaceRange,
61
61
  },
62
62
  {
63
63
  label: "../",
64
64
  kind: "folder",
65
65
  insertText: "../",
66
66
  sortText: "0_../",
67
- replaceFromColumn: valueStartColumn,
67
+ replaceRange,
68
68
  },
69
69
  ];
70
70
  }
@@ -95,14 +95,14 @@ async function relativePathCompletions(prefix, valueStartColumn, adapter) {
95
95
  detail: isModule ? "telo module" : "folder",
96
96
  insertText: fullPath,
97
97
  filterText: fullPath,
98
- replaceFromColumn: valueStartColumn,
98
+ replaceRange,
99
99
  // Modules sort above plain folders so they surface first when the user
100
100
  // is browsing a `modules/` tree mixed with non-Telo siblings.
101
101
  sortText: isModule ? `0_${name}` : `1_${name}`,
102
102
  };
103
103
  }));
104
104
  }
105
- async function refSearchCompletions(prefix, valueStartColumn, adapter) {
105
+ async function refSearchCompletions(prefix, replaceRange, adapter) {
106
106
  // The whole typed prefix is the fuzzy query — the hub matches it as a
107
107
  // substring over each registered ref, so no client-side splitting is needed
108
108
  // (and `oci://ghcr.io/aws/telo-s3` matches without mangling the `//`).
@@ -124,7 +124,7 @@ async function refSearchCompletions(prefix, valueStartColumn, adapter) {
124
124
  documentation: m.description ? m.ref : undefined,
125
125
  insertText: id,
126
126
  filterText: id,
127
- replaceFromColumn: valueStartColumn,
127
+ replaceRange,
128
128
  };
129
129
  });
130
130
  }
@@ -138,7 +138,7 @@ function refDisplayName(ref) {
138
138
  const segments = withoutScheme.split("/").filter(Boolean);
139
139
  return segments.slice(-2).join("/") || ref;
140
140
  }
141
- async function versionCompletions(prefix, atIdx, valueStartColumn, adapter) {
141
+ async function versionCompletions(prefix, atIdx, replaceRange, adapter) {
142
142
  const ref = prefix.slice(0, atIdx);
143
143
  const partialVersion = prefix.slice(atIdx + 1);
144
144
  if (ref === "")
@@ -157,7 +157,7 @@ async function versionCompletions(prefix, atIdx, valueStartColumn, adapter) {
157
157
  detail: idx === 0 ? "latest" : undefined,
158
158
  insertText: id,
159
159
  filterText: id,
160
- replaceFromColumn: valueStartColumn,
160
+ replaceRange,
161
161
  // Preserve the hub's ordering (newest first) so the latest version is
162
162
  // suggested at the top regardless of lexical comparison.
163
163
  sortText: String(idx).padStart(4, "0"),
@@ -0,0 +1,50 @@
1
+ import { type AstDocument, type AstMap, type AstNode, type CelSegment, type Position } from "@telorun/analyzer";
2
+ /** The cursor's resolved position against the read-only AST. `locate` produces
3
+ * this; `detect-context` maps it onto a `CompletionCtx`. Structure comes from
4
+ * the AST; the cursor *column* is used only to resolve empty-space (blank /
5
+ * trailing-indent) key positions, where indentation is the sole signal for
6
+ * "which container am I typing into". */
7
+ export interface ResolvedCursor {
8
+ docIndex: number;
9
+ /** Top-level `kind:` value of the cursor's document, when present. */
10
+ docKind?: string;
11
+ slot: "key" | "value";
12
+ /** Value slot: ancestor keys + the field key (last element is the field).
13
+ * Key slot: the container map's ancestor key chain. */
14
+ path: string[];
15
+ node?: AstNode;
16
+ container?: AstMap;
17
+ replaceRange?: {
18
+ start: Position;
19
+ end: Position;
20
+ };
21
+ /** Value slot: text from the value start up to the cursor. */
22
+ prefix?: string;
23
+ /** Value slot: true when whitespace separates the key's colon from the value
24
+ * (distinguishes `Console: ` — a value — from `Tiny:` — a bare header). */
25
+ spaceAfterColon?: boolean;
26
+ /** Value slot: the value of a sibling `kind:` in the same map (object-form
27
+ * ref name completion). */
28
+ siblingKind?: string;
29
+ /** Key slot: keys already present in the container (the key under the cursor
30
+ * excluded so it still suggests itself). */
31
+ existingKeys?: Set<string>;
32
+ /** Key slot: the kind of the nearest enclosing inline resource (or the root
33
+ * resource), whose schema the prop keys are completed against. Falls back to
34
+ * the document kind at the root. */
35
+ resourceKind?: string;
36
+ /** Key slot: number of `path` segments that reach `resourceKind`'s map, so
37
+ * the schema-relative path is `path.slice(resourceDepth)`. */
38
+ resourceDepth?: number;
39
+ /** Set when the cursor sits inside a CEL body (closed or open). Populated for
40
+ * a future CEL-completion feature; this refactor does not consume it. */
41
+ cel?: {
42
+ segment: CelSegment;
43
+ offset: number;
44
+ };
45
+ }
46
+ export declare function scalarString(node: AstNode | undefined): string | undefined;
47
+ /** Resolve `(line, character)` against the AST (Approach B: AST for structure,
48
+ * cursor column only to place empty-space key positions). */
49
+ export declare function resolveNodeAtPosition(text: string, docs: AstDocument[], line: number, character: number): ResolvedCursor | undefined;
50
+ //# sourceMappingURL=resolve-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-node.d.ts","sourceRoot":"","sources":["../../src/completions/resolve-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,KAAK,OAAO,EAEZ,KAAK,UAAU,EACf,KAAK,QAAQ,EACd,MAAM,mBAAmB,CAAC;AAE3B;;;;0CAI0C;AAC1C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;IACtB;4DACwD;IACxD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE;QAAE,KAAK,EAAE,QAAQ,CAAC;QAAC,GAAG,EAAE,QAAQ,CAAA;KAAE,CAAC;IAClD,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;gFAC4E;IAC5E,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;gCAC4B;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;iDAC6C;IAC7C,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B;;yCAEqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;mEAC+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;8EAC0E;IAC1E,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,UAAU,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAMD,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAG1E;AAwPD;8DAC8D;AAC9D,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,WAAW,EAAE,EACnB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,cAAc,GAAG,SAAS,CA4F5B"}
@@ -0,0 +1,269 @@
1
+ import { buildLineOffsets, offsetToPosition, } from "@telorun/analyzer";
2
+ function within(range, offset) {
3
+ return offset >= range[0] && offset <= range[1];
4
+ }
5
+ export function scalarString(node) {
6
+ if (node?.kind === "scalar" && typeof node.value === "string")
7
+ return node.value;
8
+ return undefined;
9
+ }
10
+ /** The `kind:` value of a document's root map, if any. */
11
+ function docKindOf(doc) {
12
+ if (doc.root?.kind !== "map")
13
+ return undefined;
14
+ for (const pair of doc.root.entries) {
15
+ if (scalarString(pair.key) === "kind")
16
+ return scalarString(pair.value);
17
+ }
18
+ return undefined;
19
+ }
20
+ /** Value of a sibling `kind:` entry in `map`, for object-form ref detection. */
21
+ function siblingKindOf(map) {
22
+ for (const pair of map.entries) {
23
+ if (scalarString(pair.key) === "kind")
24
+ return scalarString(pair.value);
25
+ }
26
+ return undefined;
27
+ }
28
+ /** The map's `kind:` value when it names a resource kind (`Alias.Kind`), i.e.
29
+ * the map is an inline resource. A prop-key position inside such a map is
30
+ * completed against *this* kind's schema, not the outer ref slot's. */
31
+ function resourceKindOf(map) {
32
+ const kind = siblingKindOf(map);
33
+ return kind && /^\w+\.\w+/.test(kind) ? kind : undefined;
34
+ }
35
+ function enter(scope, map, ancestorsLen) {
36
+ const kind = resourceKindOf(map);
37
+ return kind ? { kind, depth: ancestorsLen } : scope;
38
+ }
39
+ function descend(node, ancestors, offset, scope) {
40
+ if (node.kind === "map") {
41
+ const mapScope = enter(scope, node, ancestors.length);
42
+ for (const pair of node.entries) {
43
+ const keyName = scalarString(pair.key);
44
+ if (within(pair.key.range, offset)) {
45
+ return {
46
+ type: "key",
47
+ container: node,
48
+ path: ancestors,
49
+ keyNode: pair.key,
50
+ keyName,
51
+ scope: mapScope,
52
+ };
53
+ }
54
+ if (pair.value && within(pair.value.range, offset)) {
55
+ const childAncestors = keyName != null ? [...ancestors, keyName] : ancestors;
56
+ if (pair.value.kind === "map" || pair.value.kind === "seq") {
57
+ return descend(pair.value, childAncestors, offset, mapScope) ?? { type: "empty" };
58
+ }
59
+ return {
60
+ type: "value",
61
+ container: node,
62
+ path: ancestors,
63
+ keyName,
64
+ keyEnd: pair.key.range[1],
65
+ valueNode: pair.value,
66
+ };
67
+ }
68
+ }
69
+ return undefined;
70
+ }
71
+ if (node.kind === "seq") {
72
+ // Sequence items are transparent to the key path (mirrors the schema
73
+ // walker, which auto-descends arrays).
74
+ for (const item of node.items) {
75
+ if (within(item.range, offset)) {
76
+ if (item.kind === "map" || item.kind === "seq") {
77
+ return descend(item, ancestors, offset, scope) ?? { type: "empty" };
78
+ }
79
+ // A bare scalar list item (`targets:\n - One`) has no enclosing map of
80
+ // keyed siblings — leave `container` undefined rather than treating the
81
+ // seq as a map.
82
+ return { type: "value", container: undefined, path: ancestors, keyEnd: item.range[0], valueNode: item };
83
+ }
84
+ }
85
+ return undefined;
86
+ }
87
+ return undefined;
88
+ }
89
+ function collectScopes(node, ancestors, scope, lineOffsets, maps, pairs) {
90
+ if (node.kind === "map") {
91
+ const mapScope = enter(scope, node, ancestors.length);
92
+ const keys = new Set();
93
+ let childColumn = -1;
94
+ for (const pair of node.entries) {
95
+ const keyName = scalarString(pair.key);
96
+ if (keyName != null)
97
+ keys.add(keyName);
98
+ if (childColumn < 0)
99
+ childColumn = offsetToPosition(pair.key.range[0], lineOffsets).character;
100
+ }
101
+ if (childColumn >= 0) {
102
+ maps.push({ path: ancestors, childColumn, keys, rangeStart: node.range[0], scope: mapScope });
103
+ }
104
+ for (const pair of node.entries) {
105
+ const keyName = scalarString(pair.key);
106
+ const fullPath = keyName != null ? [...ancestors, keyName] : ancestors;
107
+ const childKeys = new Set();
108
+ if (pair.value?.kind === "map") {
109
+ for (const p of pair.value.entries) {
110
+ const k = scalarString(p.key);
111
+ if (k != null)
112
+ childKeys.add(k);
113
+ }
114
+ }
115
+ pairs.push({
116
+ path: fullPath,
117
+ keyColumn: offsetToPosition(pair.key.range[0], lineOffsets).character,
118
+ keyOffset: pair.key.range[0],
119
+ childKeys,
120
+ scope: mapScope,
121
+ });
122
+ if (pair.value)
123
+ collectScopes(pair.value, fullPath, mapScope, lineOffsets, maps, pairs);
124
+ }
125
+ }
126
+ else if (node.kind === "seq") {
127
+ for (const item of node.items)
128
+ collectScopes(item, ancestors, scope, lineOffsets, maps, pairs);
129
+ }
130
+ }
131
+ /** Resolve the container a new key at `cursorColumn` belongs to. Prefers an
132
+ * existing sibling level (a map whose child keys sit at exactly `cursorColumn`);
133
+ * otherwise nests under the nearest-preceding shallower key. */
134
+ function columnSearch(root, cursorColumn, cursorOffset, lineOffsets) {
135
+ const maps = [];
136
+ const pairs = [];
137
+ collectScopes(root, [], { depth: 0 }, lineOffsets, maps, pairs);
138
+ // Sibling level: a map whose children already sit at the cursor's column.
139
+ let sibling;
140
+ for (const m of maps) {
141
+ if (m.childColumn === cursorColumn && m.rangeStart < cursorOffset) {
142
+ if (!sibling || m.rangeStart > sibling.rangeStart)
143
+ sibling = m;
144
+ }
145
+ }
146
+ if (sibling)
147
+ return { path: sibling.path, existingKeys: sibling.keys, scope: sibling.scope };
148
+ // Nest under the nearest-preceding key shallower than the cursor.
149
+ let nest;
150
+ for (const p of pairs) {
151
+ if (p.keyColumn < cursorColumn && p.keyOffset < cursorOffset) {
152
+ if (!nest ||
153
+ p.keyColumn > nest.keyColumn ||
154
+ (p.keyColumn === nest.keyColumn && p.keyOffset > nest.keyOffset)) {
155
+ nest = p;
156
+ }
157
+ }
158
+ }
159
+ if (nest)
160
+ return { path: nest.path, existingKeys: nest.childKeys, scope: nest.scope };
161
+ return { path: [], existingKeys: new Set(), scope: { depth: 0 } };
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ function selectDoc(docs, offset) {
165
+ let best = -1;
166
+ for (let i = 0; i < docs.length; i++) {
167
+ if (docs[i].range[0] <= offset)
168
+ best = i;
169
+ }
170
+ return best < 0 ? (docs.length > 0 ? 0 : -1) : best;
171
+ }
172
+ function celAt(node, offset) {
173
+ for (const segment of node.celSegments()) {
174
+ if (offset >= segment.range[0] && offset <= segment.range[1])
175
+ return { segment, offset };
176
+ }
177
+ return undefined;
178
+ }
179
+ /** Resolve `(line, character)` against the AST (Approach B: AST for structure,
180
+ * cursor column only to place empty-space key positions). */
181
+ export function resolveNodeAtPosition(text, docs, line, character) {
182
+ if (docs.length === 0)
183
+ return undefined;
184
+ const lineOffsets = buildLineOffsets(text);
185
+ const offset = (lineOffsets[line] ?? 0) + character;
186
+ const toPos = (o) => offsetToPosition(o, lineOffsets);
187
+ const docIndex = selectDoc(docs, offset);
188
+ if (docIndex < 0)
189
+ return undefined;
190
+ const doc = docs[docIndex];
191
+ const docKind = docKindOf(doc);
192
+ const found = doc.root ? descend(doc.root, [], offset, { depth: 0 }) : undefined;
193
+ // Cursor sits on an existing map key → key/prop-key position.
194
+ if (found?.type === "key") {
195
+ const existingKeys = new Set();
196
+ for (const pair of found.container.entries) {
197
+ const k = scalarString(pair.key);
198
+ if (k != null && k !== found.keyName)
199
+ existingKeys.add(k);
200
+ }
201
+ return {
202
+ docIndex,
203
+ docKind,
204
+ slot: "key",
205
+ path: found.path,
206
+ node: found.keyNode,
207
+ replaceRange: { start: toPos(found.keyNode.range[0]), end: toPos(found.keyNode.range[1]) },
208
+ container: found.container,
209
+ existingKeys,
210
+ resourceKind: found.scope.kind,
211
+ resourceDepth: found.scope.depth,
212
+ };
213
+ }
214
+ // Cursor sits on a scalar value.
215
+ if (found?.type === "value" && found.valueNode.kind === "scalar") {
216
+ const value = found.valueNode;
217
+ const cel = celAt(value, offset);
218
+ // A bare identifier on its own line with no colon is a partial *key* being
219
+ // typed as a first child (yaml parses it as the parent's value). Route to a
220
+ // key position via column search — the documented cursor-line carve-out.
221
+ const lineText = text.slice(lineOffsets[line] ?? 0, lineOffsets[line + 1] ?? text.length);
222
+ const isPartialKey = typeof value.value === "string" &&
223
+ !lineText.includes(":") &&
224
+ /^\s*[A-Za-z_][\w-]*\s*$/.test(lineText) &&
225
+ toPos(value.range[0]).line !== toPos(found.keyEnd).line;
226
+ if (isPartialKey && doc.root) {
227
+ const col = toPos(value.range[0]).character;
228
+ const { path, existingKeys, scope } = columnSearch(doc.root, col, offset, lineOffsets);
229
+ return {
230
+ docIndex,
231
+ docKind,
232
+ slot: "key",
233
+ path,
234
+ container: found.container,
235
+ existingKeys,
236
+ resourceKind: scope.kind,
237
+ resourceDepth: scope.depth,
238
+ };
239
+ }
240
+ const clampedEnd = Math.min(offset, value.range[1]);
241
+ return {
242
+ docIndex,
243
+ docKind,
244
+ slot: "value",
245
+ path: found.keyName != null ? [...found.path, found.keyName] : found.path,
246
+ node: value,
247
+ container: found.container,
248
+ prefix: text.slice(value.range[0], clampedEnd),
249
+ spaceAfterColon: value.range[0] - found.keyEnd >= 2,
250
+ siblingKind: found.container ? siblingKindOf(found.container) : undefined,
251
+ replaceRange: { start: toPos(value.range[0]), end: toPos(value.range[1]) },
252
+ cel,
253
+ };
254
+ }
255
+ // Empty space (blank line, trailing indent, empty document) → key position,
256
+ // resolved by cursor column.
257
+ const resolution = doc.root
258
+ ? columnSearch(doc.root, character, offset, lineOffsets)
259
+ : { path: [], existingKeys: new Set(), scope: { depth: 0 } };
260
+ return {
261
+ docIndex,
262
+ docKind,
263
+ slot: "key",
264
+ path: resolution.path,
265
+ existingKeys: resolution.existingKeys,
266
+ resourceKind: resolution.scope.kind,
267
+ resourceDepth: resolution.scope.depth,
268
+ };
269
+ }
@@ -1,2 +1,5 @@
1
1
  export declare const CAPABILITY_VALUES: readonly ["Telo.Service", "Telo.Runnable", "Telo.Invocable", "Telo.Provider", "Telo.Mount", "Telo.Type"];
2
+ /** One-line role summary per capability, surfaced on hover. Kept in sync with
3
+ * the capability list in `CLAUDE.md` / the kernel builtins. */
4
+ export declare const CAPABILITY_DOCS: Record<string, string>;
2
5
  //# sourceMappingURL=valid-capabilities.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"valid-capabilities.d.ts","sourceRoot":"","sources":["../../src/completions/valid-capabilities.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,0GAOpB,CAAC"}
1
+ {"version":3,"file":"valid-capabilities.d.ts","sourceRoot":"","sources":["../../src/completions/valid-capabilities.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,0GAOpB,CAAC;AAEX;gEACgE;AAChE,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOlD,CAAC"}
@@ -6,3 +6,13 @@ export const CAPABILITY_VALUES = [
6
6
  "Telo.Mount",
7
7
  "Telo.Type",
8
8
  ];
9
+ /** One-line role summary per capability, surfaced on hover. Kept in sync with
10
+ * the capability list in `CLAUDE.md` / the kernel builtins. */
11
+ export const CAPABILITY_DOCS = {
12
+ "Telo.Service": "Long-lived resource: `init()` + optional `teardown()` (servers, pools).",
13
+ "Telo.Runnable": "One-shot task: `run()` (pipelines, boot steps).",
14
+ "Telo.Invocable": "Request handler: `invoke(inputs)` (scripts, endpoints).",
15
+ "Telo.Provider": "Value-flow source: `init()` + optional `provide()` (config, secrets).",
16
+ "Telo.Mount": "Mounted into a Service (HTTP APIs, middleware).",
17
+ "Telo.Type": "Pure schema definition — no runtime instance.",
18
+ };
@@ -0,0 +1,13 @@
1
+ import { type AstDocument, type LoadedGraph } from "@telorun/analyzer";
2
+ import type { DefinitionResult } from "../types.js";
3
+ /** Resolve the `!ref` under the cursor to its target resource's definition.
4
+ *
5
+ * The ref grammar mirrors `resolveRefSentinels`: the tag's value is split on
6
+ * the first dot — a bare name (or `Self.name`) is a local resource in the
7
+ * current module; `Alias.name` is an exported instance of the module the import
8
+ * `Alias` points at, followed transitively through re-exports and gated on each
9
+ * module's `exports.resources`. Returns `undefined` when the cursor isn't on a
10
+ * `!ref`, or the target can't be found (e.g. a scope-local name, an unexported
11
+ * instance, or an import that failed to load). */
12
+ export declare function buildDefinition(text: string, line: number, character: number, graph: LoadedGraph, currentFilePath: string, docs?: AstDocument[]): DefinitionResult | undefined;
13
+ //# sourceMappingURL=build-definition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-definition.d.ts","sourceRoot":"","sources":["../../src/definition/build-definition.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAEhB,KAAK,WAAW,EAGjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA6EpD;;;;;;;;mDAQmD;AACnD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,WAAW,EAClB,eAAe,EAAE,MAAM,EACvB,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,gBAAgB,GAAG,SAAS,CAqB9B"}
@@ -0,0 +1,98 @@
1
+ import { parseToAst, } from "@telorun/analyzer";
2
+ import { resolveNodeAtPosition } from "../completions/resolve-node.js";
3
+ /** The module whose owner or partials includes `filePath`. */
4
+ function moduleForFile(graph, filePath) {
5
+ for (const mod of graph.modules.values()) {
6
+ if (mod.owner.source === filePath)
7
+ return mod;
8
+ if (mod.partials.some((p) => p.source === filePath))
9
+ return mod;
10
+ }
11
+ return undefined;
12
+ }
13
+ /** First resource named `name` across `files`, located at its `metadata.name`
14
+ * (or its first line as a fallback). Names are unique within a module scope, so
15
+ * the first hit is the definition. */
16
+ function locateResource(files, name) {
17
+ for (const file of files) {
18
+ for (let i = 0; i < file.manifests.length; i++) {
19
+ const manifest = file.manifests[i];
20
+ if (!manifest || manifest.metadata?.name !== name)
21
+ continue;
22
+ const pos = file.positions[i];
23
+ const range = pos?.positionIndex.get("metadata.name") ??
24
+ pos?.positionIndex.get("@key:metadata.name") ??
25
+ (pos ? { start: { line: pos.sourceLine, character: 0 }, end: { line: pos.sourceLine, character: 0 } } : undefined);
26
+ if (range)
27
+ return { uri: file.source, range };
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ /** The `exports.resources` list of a module's owner doc (empty when absent). */
33
+ function exportedResources(mod) {
34
+ const doc = mod.owner.manifests.find((m) => m?.kind === "Telo.Library" || m?.kind === "Telo.Application");
35
+ const list = doc?.exports?.resources;
36
+ return Array.isArray(list) ? list.filter((e) => typeof e === "string") : [];
37
+ }
38
+ /** Follow `name` into `moduleSource` across the export boundary, honoring the
39
+ * `exports.resources` gate and re-export chains (`app → api → domain → …`). A
40
+ * terminal match is a locally-owned instance the module actually exports; a
41
+ * re-export entry `InnerAlias.name` hops through that module's own import edge.
42
+ * `seen` bounds cyclic import graphs. */
43
+ function resolveExported(graph, moduleSource, name, seen = new Set()) {
44
+ if (seen.has(moduleSource))
45
+ return undefined;
46
+ seen.add(moduleSource);
47
+ const mod = graph.modules.get(moduleSource);
48
+ if (!mod)
49
+ return undefined;
50
+ const exports = exportedResources(mod);
51
+ // No `exports.resources` block → ungated (the module hasn't opted into the
52
+ // gate, same as `exports.kinds`): a plain name match keeps navigation working
53
+ // for modules that predate explicit exports.
54
+ if (exports.length === 0) {
55
+ return locateResource([mod.owner, ...mod.partials], name);
56
+ }
57
+ if (exports.includes(name)) {
58
+ const local = locateResource([mod.owner, ...mod.partials], name);
59
+ if (local)
60
+ return local;
61
+ }
62
+ const reexport = exports.find((e) => e.endsWith(`.${name}`));
63
+ if (!reexport)
64
+ return undefined;
65
+ const innerAlias = reexport.slice(0, reexport.length - name.length - 1);
66
+ const edge = graph.importEdges.get(mod.owner.source)?.get(innerAlias);
67
+ return edge ? resolveExported(graph, edge.targetSource, name, seen) : undefined;
68
+ }
69
+ /** Resolve the `!ref` under the cursor to its target resource's definition.
70
+ *
71
+ * The ref grammar mirrors `resolveRefSentinels`: the tag's value is split on
72
+ * the first dot — a bare name (or `Self.name`) is a local resource in the
73
+ * current module; `Alias.name` is an exported instance of the module the import
74
+ * `Alias` points at, followed transitively through re-exports and gated on each
75
+ * module's `exports.resources`. Returns `undefined` when the cursor isn't on a
76
+ * `!ref`, or the target can't be found (e.g. a scope-local name, an unexported
77
+ * instance, or an import that failed to load). */
78
+ export function buildDefinition(text, line, character, graph, currentFilePath, docs) {
79
+ const astDocs = docs ?? parseToAst(text);
80
+ const node = resolveNodeAtPosition(text, astDocs, line, character)?.node;
81
+ if (!node || node.kind !== "scalar" || node.tag !== "!ref")
82
+ return undefined;
83
+ // The scalar's range covers the ref target text (the value after `!ref`).
84
+ const source = text.slice(node.range[0], node.range[1]).trim();
85
+ if (!source)
86
+ return undefined;
87
+ const currentModule = moduleForFile(graph, currentFilePath) ?? graph.entry;
88
+ const dot = source.indexOf(".");
89
+ const alias = dot === -1 ? undefined : source.slice(0, dot);
90
+ const name = dot === -1 ? source : source.slice(dot + 1);
91
+ if (alias === undefined || alias === "Self") {
92
+ return locateResource([currentModule.owner, ...currentModule.partials], name);
93
+ }
94
+ const edge = graph.importEdges.get(currentModule.owner.source)?.get(alias);
95
+ if (!edge)
96
+ return undefined;
97
+ return resolveExported(graph, edge.targetSource, name);
98
+ }
@@ -0,0 +1,2 @@
1
+ export { buildDefinition } from "./build-definition.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/definition/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1 @@
1
+ export { buildDefinition } from "./build-definition.js";
@@ -0,0 +1,4 @@
1
+ import { type AnalysisRegistry, type AstDocument } from "@telorun/analyzer";
2
+ import type { HoverResult } from "../types.js";
3
+ export declare function buildHover(text: string, line: number, character: number, registry: AnalysisRegistry | undefined, docs?: AstDocument[]): HoverResult | undefined;
4
+ //# sourceMappingURL=build-hover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-hover.d.ts","sourceRoot":"","sources":["../../src/hover/build-hover.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAoF/C,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,WAAW,GAAG,SAAS,CAOzB"}