@telorun/ide-support 0.5.0 → 0.7.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 (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 +12 -8
  8. package/dist/completions/import-source.d.ts.map +1 -1
  9. package/dist/completions/import-source.js +67 -49
  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 +66 -18
  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 +71 -53
  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 +78 -18
@@ -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
@@ -7,13 +7,17 @@ import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
7
7
  * Branches by prefix shape:
8
8
  * "" → relative dirs under the manifest dir, plus `./` / `../` seeds.
9
9
  * "./..", "../", "/..." → subdirs of the typed path (any subdir; existing manifest gets a hint).
10
- * "<word>" registry search by free-text.
11
- * "<ns>/<name>@<partial>" → version list for that module.
12
- * "http(s)://", "file://" → no suggestions (opaque URLs).
10
+ * "<word>", "oci://…" hub ref autocomplete (fuzzy substring over registered refs).
11
+ * "<ref>@<partial>" → version list for that ref.
12
+ * "http(s)://", "file://" → no suggestions (opaque URLs the author types verbatim).
13
13
  *
14
- * `valueStartColumn` is forwarded onto every result so the host can replace
15
- * the whole typed value, not just the trailing word (Monaco / VSCode word
16
- * boundaries don't cross `/` or `@`).
14
+ * `oci://` is deliberately NOT opaque: without an `@` it routes to the ref
15
+ * search below, whose query is the whole typed prefix so the hub fuzzy-matches
16
+ * `oci://ghcr.io/aws/telo-s3` as readily as a bare `s3`.
17
+ *
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 `@`).
17
21
  */
18
- 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[]>;
19
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;AAW3E;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,qBAAqB,GAAG,SAAS,GACzC,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuB7B"}
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"}
@@ -1,6 +1,7 @@
1
- /** Maximum registry hits to surface in a single completion request.
2
- * Keeps the popover scannable when a broad `q=` query matches the catalog. */
3
- const REGISTRY_LIMIT = 50;
1
+ /** Maximum ref hits to surface in a single completion request. Keeps the
2
+ * popover scannable when a broad `q=` query matches many registered refs
3
+ * (the hub already caps `/refs` server-side; this is a client backstop). */
4
+ const REF_LIMIT = 50;
4
5
  /** Caps the number of directory entries we probe with `hasManifest` per
5
6
  * request. Each probe is a host-side filesystem stat; the popover would not
6
7
  * show more than ~50 entries anyway, so probing further only adds latency. */
@@ -13,15 +14,19 @@ const PATH_PROBE_LIMIT = 50;
13
14
  * Branches by prefix shape:
14
15
  * "" → relative dirs under the manifest dir, plus `./` / `../` seeds.
15
16
  * "./..", "../", "/..." → subdirs of the typed path (any subdir; existing manifest gets a hint).
16
- * "<word>" registry search by free-text.
17
- * "<ns>/<name>@<partial>" → version list for that module.
18
- * "http(s)://", "file://" → no suggestions (opaque URLs).
17
+ * "<word>", "oci://…" hub ref autocomplete (fuzzy substring over registered refs).
18
+ * "<ref>@<partial>" → version list for that ref.
19
+ * "http(s)://", "file://" → no suggestions (opaque URLs the author types verbatim).
19
20
  *
20
- * `valueStartColumn` is forwarded onto every result so the host can replace
21
- * the whole typed value, not just the trailing word (Monaco / VSCode word
22
- * boundaries don't cross `/` or `@`).
21
+ * `oci://` is deliberately NOT opaque: without an `@` it routes to the ref
22
+ * search below, whose query is the whole typed prefix so the hub fuzzy-matches
23
+ * `oci://ghcr.io/aws/telo-s3` as readily as a bare `s3`.
24
+ *
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 `@`).
23
28
  */
24
- export async function importSourceCompletions(prefix, valueStartColumn, adapter) {
29
+ export async function importSourceCompletions(prefix, replaceRange, adapter) {
25
30
  if (!adapter)
26
31
  return [];
27
32
  if (prefix.startsWith("http://") ||
@@ -31,15 +36,17 @@ export async function importSourceCompletions(prefix, valueStartColumn, adapter)
31
36
  }
32
37
  const isRelativeShape = prefix === "" || prefix.startsWith(".") || prefix.startsWith("/");
33
38
  if (isRelativeShape) {
34
- return relativePathCompletions(prefix, valueStartColumn, adapter);
39
+ return relativePathCompletions(prefix, replaceRange, adapter);
35
40
  }
36
- const atIdx = prefix.indexOf("@");
41
+ // The version (or `@sha256:` digest) is the trailing `@`-segment, so split on
42
+ // the LAST `@` — a digest-pinned ref keeps everything before it as the ref.
43
+ const atIdx = prefix.lastIndexOf("@");
37
44
  if (atIdx > 0) {
38
- return versionCompletions(prefix, atIdx, valueStartColumn, adapter);
45
+ return versionCompletions(prefix, atIdx, replaceRange, adapter);
39
46
  }
40
- return registrySearchCompletions(prefix, valueStartColumn, adapter);
47
+ return refSearchCompletions(prefix, replaceRange, adapter);
41
48
  }
42
- async function relativePathCompletions(prefix, valueStartColumn, adapter) {
49
+ async function relativePathCompletions(prefix, replaceRange, adapter) {
43
50
  // Empty prefix → seed `./` and `../` so the user gets traction; otherwise
44
51
  // we'd return an unfiltered dump of the manifest directory which is rarely
45
52
  // what the user wants for an import source.
@@ -50,14 +57,14 @@ async function relativePathCompletions(prefix, valueStartColumn, adapter) {
50
57
  kind: "folder",
51
58
  insertText: "./",
52
59
  sortText: "0_./",
53
- replaceFromColumn: valueStartColumn,
60
+ replaceRange,
54
61
  },
55
62
  {
56
63
  label: "../",
57
64
  kind: "folder",
58
65
  insertText: "../",
59
66
  sortText: "0_../",
60
- replaceFromColumn: valueStartColumn,
67
+ replaceRange,
61
68
  },
62
69
  ];
63
70
  }
@@ -88,59 +95,70 @@ async function relativePathCompletions(prefix, valueStartColumn, adapter) {
88
95
  detail: isModule ? "telo module" : "folder",
89
96
  insertText: fullPath,
90
97
  filterText: fullPath,
91
- replaceFromColumn: valueStartColumn,
98
+ replaceRange,
92
99
  // Modules sort above plain folders so they surface first when the user
93
100
  // is browsing a `modules/` tree mixed with non-Telo siblings.
94
101
  sortText: isModule ? `0_${name}` : `1_${name}`,
95
102
  };
96
103
  }));
97
104
  }
98
- async function registrySearchCompletions(prefix, valueStartColumn, adapter) {
99
- // The registry's `q` filter ILIKEs against name / namespace / description
100
- // it doesn't know about the `<namespace>/<name>` shape. Once the user has
101
- // typed a `/`, sending the literal `std/htt` as `q` matches nothing because
102
- // the slash is not in any of those columns. Split here so `q` carries just
103
- // the bit that looks like a name, and apply the namespace constraint
104
- // client-side.
105
- const slashIdx = prefix.indexOf("/");
106
- const namespacePart = slashIdx >= 0 ? prefix.slice(0, slashIdx) : "";
107
- const namePart = slashIdx >= 0 ? prefix.slice(slashIdx + 1) : prefix;
108
- const hits = await adapter.searchRegistry(namePart);
109
- const filtered = namespacePart
110
- ? hits.filter((h) => h.namespace.startsWith(namespacePart))
111
- : hits;
112
- return filtered.slice(0, REGISTRY_LIMIT).map((m) => {
113
- const id = `${m.namespace}/${m.name}@${m.version}`;
105
+ async function refSearchCompletions(prefix, replaceRange, adapter) {
106
+ // The whole typed prefix is the fuzzy query the hub matches it as a
107
+ // substring over each registered ref, so no client-side splitting is needed
108
+ // (and `oci://ghcr.io/aws/telo-s3` matches without mangling the `//`).
109
+ const hits = await adapter.searchRefs(prefix);
110
+ return hits.slice(0, REF_LIMIT).map((m) => {
111
+ // Seed the pinned `ref@latestVersion` so the completion is directly usable;
112
+ // the author can still narrow the version afterwards (the `@` re-triggers
113
+ // version completion).
114
+ const id = m.latestVersion ? `${m.ref}@${m.latestVersion}` : m.ref;
115
+ const name = refDisplayName(m.ref);
116
+ // Lead the label with the module name so the interesting part isn't cut off
117
+ // behind the transport/host boilerplate (`oci://ghcr.io/telorun/…`). The
118
+ // full ref moves to `detail`, and `insertText`/`filterText` stay the ref so
119
+ // acceptance still inserts it and a fully-typed ref still filters.
114
120
  return {
115
- label: id,
121
+ label: m.latestVersion ? `${name}@${m.latestVersion}` : name,
116
122
  kind: "module",
117
- detail: m.description ?? "registry module",
123
+ detail: m.description ?? m.ref,
124
+ documentation: m.description ? m.ref : undefined,
118
125
  insertText: id,
119
126
  filterText: id,
120
- replaceFromColumn: valueStartColumn,
127
+ replaceRange,
121
128
  };
122
129
  });
123
130
  }
124
- async function versionCompletions(prefix, atIdx, valueStartColumn, adapter) {
125
- const beforeAt = prefix.slice(0, atIdx);
131
+ /** The `org/name` tail of a location ref: its last two path segments, with the
132
+ * transport scheme (`oci://`, `https://`, …) and registry host dropped.
133
+ * `oci://ghcr.io/telorun/telo-console` → `telorun/telo-console`; `std/console`
134
+ * → `std/console`. Falls back to fewer segments (or the whole ref) when there
135
+ * aren't two. */
136
+ function refDisplayName(ref) {
137
+ const withoutScheme = ref.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
138
+ const segments = withoutScheme.split("/").filter(Boolean);
139
+ return segments.slice(-2).join("/") || ref;
140
+ }
141
+ async function versionCompletions(prefix, atIdx, replaceRange, adapter) {
142
+ const ref = prefix.slice(0, atIdx);
126
143
  const partialVersion = prefix.slice(atIdx + 1);
127
- const slashIdx = beforeAt.indexOf("/");
128
- if (slashIdx <= 0 || slashIdx === beforeAt.length - 1)
144
+ if (ref === "")
129
145
  return [];
130
- const namespace = beforeAt.slice(0, slashIdx);
131
- const name = beforeAt.slice(slashIdx + 1);
132
- const versions = await adapter.listRegistryVersions(namespace, name);
146
+ const versions = await adapter.listVersionsForRef(ref);
133
147
  const matches = versions.filter((v) => v.startsWith(partialVersion));
134
148
  return matches.map((version, idx) => {
135
- const id = `${namespace}/${name}@${version}`;
149
+ const id = `${ref}@${version}`;
150
+ // The ref is already typed and visible on the line, so the label is just the
151
+ // version — no point repeating the full ref on every row. `insertText` /
152
+ // `filterText` stay the full id so acceptance replaces the whole value and
153
+ // the already-typed ref prefix keeps the item in the filtered set.
136
154
  return {
137
- label: id,
155
+ label: version,
138
156
  kind: "value",
139
- detail: idx === 0 ? "latest" : `v${version}`,
157
+ detail: idx === 0 ? "latest" : undefined,
140
158
  insertText: id,
141
159
  filterText: id,
142
- replaceFromColumn: valueStartColumn,
143
- // Preserve registry's ordering (newest first) so the latest version is
160
+ replaceRange,
161
+ // Preserve the hub's ordering (newest first) so the latest version is
144
162
  // suggested at the top regardless of lexical comparison.
145
163
  sortText: String(idx).padStart(4, "0"),
146
164
  };
@@ -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"}