@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.
- package/dist/completions/build.d.ts +2 -2
- package/dist/completions/build.d.ts.map +1 -1
- package/dist/completions/build.js +39 -64
- package/dist/completions/detect-context.d.ts +16 -43
- package/dist/completions/detect-context.d.ts.map +1 -1
- package/dist/completions/detect-context.js +63 -262
- package/dist/completions/import-source.d.ts +12 -8
- package/dist/completions/import-source.d.ts.map +1 -1
- package/dist/completions/import-source.js +67 -49
- package/dist/completions/resolve-node.d.ts +50 -0
- package/dist/completions/resolve-node.d.ts.map +1 -0
- package/dist/completions/resolve-node.js +269 -0
- package/dist/completions/valid-capabilities.d.ts +3 -0
- package/dist/completions/valid-capabilities.d.ts.map +1 -1
- package/dist/completions/valid-capabilities.js +10 -0
- package/dist/definition/build-definition.d.ts +13 -0
- package/dist/definition/build-definition.d.ts.map +1 -0
- package/dist/definition/build-definition.js +98 -0
- package/dist/definition/index.d.ts +2 -0
- package/dist/definition/index.d.ts.map +1 -0
- package/dist/definition/index.js +1 -0
- package/dist/hover/build-hover.d.ts +4 -0
- package/dist/hover/build-hover.d.ts.map +1 -0
- package/dist/hover/build-hover.js +125 -0
- package/dist/hover/index.d.ts +2 -0
- package/dist/hover/index.d.ts.map +1 -0
- package/dist/hover/index.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/semantic-tokens/build-semantic-tokens.d.ts +12 -0
- package/dist/semantic-tokens/build-semantic-tokens.d.ts.map +1 -0
- package/dist/semantic-tokens/build-semantic-tokens.js +55 -0
- package/dist/semantic-tokens/index.d.ts +2 -0
- package/dist/semantic-tokens/index.d.ts.map +1 -0
- package/dist/semantic-tokens/index.js +1 -0
- package/dist/types.d.ts +66 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/package.json +2 -2
- package/src/completions/build.ts +40 -72
- package/src/completions/detect-context.ts +74 -291
- package/src/completions/import-source.ts +71 -53
- package/src/completions/resolve-node.ts +405 -0
- package/src/completions/valid-capabilities.ts +11 -0
- package/src/definition/build-definition.ts +123 -0
- package/src/definition/index.ts +1 -0
- package/src/hover/build-hover.ts +149 -0
- package/src/hover/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/semantic-tokens/build-semantic-tokens.ts +64 -0
- package/src/semantic-tokens/index.ts +1 -0
- 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>"
|
|
11
|
-
* "<
|
|
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
|
-
* `
|
|
15
|
-
* the whole typed
|
|
16
|
-
*
|
|
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,
|
|
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;
|
|
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
|
|
2
|
-
*
|
|
3
|
-
|
|
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>"
|
|
17
|
-
* "<
|
|
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
|
-
* `
|
|
21
|
-
* the whole typed
|
|
22
|
-
*
|
|
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,
|
|
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,
|
|
39
|
+
return relativePathCompletions(prefix, replaceRange, adapter);
|
|
35
40
|
}
|
|
36
|
-
|
|
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,
|
|
45
|
+
return versionCompletions(prefix, atIdx, replaceRange, adapter);
|
|
39
46
|
}
|
|
40
|
-
return
|
|
47
|
+
return refSearchCompletions(prefix, replaceRange, adapter);
|
|
41
48
|
}
|
|
42
|
-
async function relativePathCompletions(prefix,
|
|
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
|
-
|
|
60
|
+
replaceRange,
|
|
54
61
|
},
|
|
55
62
|
{
|
|
56
63
|
label: "../",
|
|
57
64
|
kind: "folder",
|
|
58
65
|
insertText: "../",
|
|
59
66
|
sortText: "0_../",
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
99
|
-
// The
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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:
|
|
121
|
+
label: m.latestVersion ? `${name}@${m.latestVersion}` : name,
|
|
116
122
|
kind: "module",
|
|
117
|
-
detail: m.description ??
|
|
123
|
+
detail: m.description ?? m.ref,
|
|
124
|
+
documentation: m.description ? m.ref : undefined,
|
|
118
125
|
insertText: id,
|
|
119
126
|
filterText: id,
|
|
120
|
-
|
|
127
|
+
replaceRange,
|
|
121
128
|
};
|
|
122
129
|
});
|
|
123
130
|
}
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
if (slashIdx <= 0 || slashIdx === beforeAt.length - 1)
|
|
144
|
+
if (ref === "")
|
|
129
145
|
return [];
|
|
130
|
-
const
|
|
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 = `${
|
|
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:
|
|
155
|
+
label: version,
|
|
138
156
|
kind: "value",
|
|
139
|
-
detail: idx === 0 ? "latest" :
|
|
157
|
+
detail: idx === 0 ? "latest" : undefined,
|
|
140
158
|
insertText: id,
|
|
141
159
|
filterText: id,
|
|
142
|
-
|
|
143
|
-
// Preserve
|
|
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"}
|