@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 { AnalysisRegistry } from "@telorun/analyzer";
1
+ import { type AnalysisRegistry, type AstDocument } from "@telorun/analyzer";
2
2
  import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
3
- export declare function buildCompletions(text: string, line: number, character: number, registry: AnalysisRegistry | undefined, adapter?: IdeEnvironmentAdapter): Promise<CompletionResult[]>;
3
+ export declare function buildCompletions(text: string, line: number, character: number, registry: AnalysisRegistry | undefined, adapter?: IdeEnvironmentAdapter, docs?: AstDocument[]): Promise<CompletionResult[]>;
4
4
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/completions/build.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAsK3E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA2B7B"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/completions/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAe,MAAM,mBAAmB,CAAC;AACrG,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAuI3E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,OAAO,CAAC,EAAE,qBAAqB,EAC/B,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyB7B"}
@@ -1,55 +1,32 @@
1
+ import { parseToAst } from "@telorun/analyzer";
1
2
  import { detectContext, lookupRefConstraint } from "./detect-context.js";
2
3
  import { importSourceCompletions } from "./import-source.js";
3
4
  import { propKeyCompletions } from "./prop-keys.js";
4
5
  import { CAPABILITY_VALUES } from "./valid-capabilities.js";
5
- /** Roughly extract `(kind, metadata.name)` pairs from a multi-doc YAML text.
6
- * This is intentionally lightweight: it scans for top-level `kind:` and the
7
- * first `name:` under a `metadata:` block per `---`-separated section, with
8
- * no full YAML parse. The output is consumed only for completion ranking,
9
- * so misses on edge-case manifests are acceptable; the analyzer remains
10
- * the source of truth for validation. */
11
- function extractInFileResources(text) {
6
+ /** Read the top-level `kind` and `metadata.name` scalar of each document from
7
+ * the AST. Consumed only for ref-name completion ranking, so a doc missing
8
+ * either is simply skipped; the analyzer remains the source of truth. */
9
+ function extractInFileResources(docs) {
12
10
  const out = [];
13
- const lines = text.split("\n");
14
- let currentKind;
15
- let currentName;
16
- let inMetadata = false;
17
- const flush = () => {
18
- if (currentKind && currentName) {
19
- out.push({ kind: currentKind, name: currentName });
20
- }
21
- currentKind = undefined;
22
- currentName = undefined;
23
- inMetadata = false;
24
- };
25
- for (const line of lines) {
26
- if (line.trimEnd() === "---") {
27
- flush();
28
- continue;
29
- }
30
- const kindMatch = line.match(/^kind:\s*(\S+)/);
31
- if (kindMatch) {
32
- currentKind = kindMatch[1];
11
+ const scalar = (node) => node?.kind === "scalar" && typeof node.value === "string" ? node.value : undefined;
12
+ for (const doc of docs) {
13
+ if (doc.root?.kind !== "map")
33
14
  continue;
34
- }
35
- if (/^metadata:\s*$/.test(line)) {
36
- inMetadata = true;
37
- continue;
38
- }
39
- if (inMetadata) {
40
- // Lines inside metadata are indented. Pick the first `name:` we see.
41
- const nameMatch = line.match(/^\s+name:\s*(\S+)/);
42
- if (nameMatch && !currentName) {
43
- currentName = nameMatch[1];
44
- }
45
- // Leaving the metadata block — any line that is not indented marks
46
- // the end of the block.
47
- if (line.length > 0 && !/^\s/.test(line)) {
48
- inMetadata = false;
15
+ let kind;
16
+ let name;
17
+ for (const pair of doc.root.entries) {
18
+ const key = scalar(pair.key);
19
+ if (key === "kind")
20
+ kind = scalar(pair.value);
21
+ else if (key === "metadata" && pair.value?.kind === "map") {
22
+ const meta = pair.value;
23
+ const nameEntry = meta.entries.find((e) => scalar(e.key) === "name");
24
+ name = scalar(nameEntry?.value);
49
25
  }
50
26
  }
27
+ if (kind && name)
28
+ out.push({ kind, name });
51
29
  }
52
- flush();
53
30
  return out;
54
31
  }
55
32
  /** Returns the resource records whose kind satisfies the slot. When the
@@ -59,8 +36,8 @@ function extractInFileResources(text) {
59
36
  * kind match. Falls back to listing every in-file resource so the
60
37
  * user still sees something rather than nothing when the registry
61
38
  * doesn't recognize the kind yet. */
62
- function refNameCompletions(text, refKind, refConstraint, registry, valueStartColumn) {
63
- const resources = extractInFileResources(text);
39
+ function refNameCompletions(docs, refKind, refConstraint, registry, replaceRange) {
40
+ const resources = extractInFileResources(docs);
64
41
  let acceptable;
65
42
  if (refKind) {
66
43
  acceptable = new Set([refKind]);
@@ -82,10 +59,9 @@ function refNameCompletions(text, refKind, refConstraint, registry, valueStartCo
82
59
  label: r.name,
83
60
  kind: "value",
84
61
  detail: r.kind,
85
- // Anchor the replace range to the value's start column so names with
86
- // `.`, `-`, or `/` (legal in resource names) replace the whole typed
87
- // prefix instead of the trailing word VS Code would pick by default.
88
- replaceFromColumn: valueStartColumn,
62
+ // Replace the whole existing value so names with `.`, `-`, or `/` (legal
63
+ // in resource names) overwrite cleanly instead of the trailing word.
64
+ replaceRange,
89
65
  });
90
66
  }
91
67
  return out;
@@ -103,7 +79,7 @@ function refConstrainedKinds(registry, parentDocKind, parentYamlPath) {
103
79
  return undefined;
104
80
  return registry.userFacingKindsForRef(refString);
105
81
  }
106
- function kindCompletions(registry, docKind, yamlPath, valueStartColumn) {
82
+ function kindCompletions(registry, docKind, yamlPath, replaceRange) {
107
83
  let kinds;
108
84
  if (registry && docKind && yamlPath && yamlPath.length > 0) {
109
85
  const filtered = refConstrainedKinds(registry, docKind, yamlPath);
@@ -121,15 +97,10 @@ function kindCompletions(registry, docKind, yamlPath, valueStartColumn) {
121
97
  if (seen.has(kind))
122
98
  continue;
123
99
  seen.add(kind);
124
- const item = { label: kind, kind: "class", detail: "Telo resource kind" };
125
- // Anchor the replace range to the value's start column so kinds with `.`
126
- // (e.g. `Sql.Connection`) cleanly overwrite the existing prefix. Without
127
- // this, VS Code's default word boundary stops at the last `.` and a pick
128
- // of `Sql.Connection` while the buffer reads `Sql.Co|` becomes
129
- // `Sql.Sql.Connection`.
130
- if (valueStartColumn !== undefined)
131
- item.replaceFromColumn = valueStartColumn;
132
- results.push(item);
100
+ // Replace the whole existing kind scalar so a pick of `Sql.Connection`
101
+ // over `Sql.Co|nnection` leaves no `nnection` suffix and no `Sql.` prefix
102
+ // duplication (VS Code's default word range stops at the last `.`).
103
+ results.push({ label: kind, kind: "class", detail: "Telo resource kind", replaceRange });
133
104
  }
134
105
  return results;
135
106
  }
@@ -140,12 +111,16 @@ function capabilityCompletions() {
140
111
  detail: "Telo capability",
141
112
  }));
142
113
  }
143
- export async function buildCompletions(text, line, character, registry, adapter) {
144
- const ctx = detectContext(text, line, character);
114
+ export async function buildCompletions(text, line, character, registry, adapter, docs) {
115
+ // Reuse the host's already-parsed AST when it matches the current buffer;
116
+ // otherwise parse once here (Part 1 stands alone). Both `detectContext` and
117
+ // ref-name in-file resource extraction share this single parse.
118
+ const astDocs = docs ?? parseToAst(text);
119
+ const ctx = detectContext(text, line, character, astDocs);
145
120
  if (!ctx)
146
121
  return [];
147
122
  if (ctx.type === "kind") {
148
- return kindCompletions(registry, ctx.docKind, ctx.yamlPath, ctx.valueStartColumn);
123
+ return kindCompletions(registry, ctx.docKind, ctx.yamlPath, ctx.replaceRange);
149
124
  }
150
125
  if (ctx.type === "capability")
151
126
  return capabilityCompletions();
@@ -154,11 +129,11 @@ export async function buildCompletions(text, line, character, registry, adapter)
154
129
  const refConstraint = definition?.schema
155
130
  ? lookupRefConstraint(definition.schema, ctx.yamlPath)
156
131
  : undefined;
157
- return refNameCompletions(text, ctx.refKind, refConstraint, registry, ctx.valueStartColumn);
132
+ return refNameCompletions(astDocs, ctx.refKind, refConstraint, registry, ctx.replaceRange);
158
133
  }
159
134
  if (ctx.type === "field-value") {
160
135
  if (ctx.field === "import-source") {
161
- return importSourceCompletions(ctx.prefix, ctx.valueStartColumn, adapter);
136
+ return importSourceCompletions(ctx.prefix, ctx.replaceRange, adapter);
162
137
  }
163
138
  return [];
164
139
  }
@@ -1,3 +1,6 @@
1
+ import { type AstDocument } from "@telorun/analyzer";
2
+ import type { ReplaceRange } from "../types.js";
3
+ export type { ReplaceRange };
1
4
  export type CompletionCtx = {
2
5
  type: "kind";
3
6
  /** Set for indented `kind:` lines. The enclosing docKind + the YAML
@@ -6,12 +9,10 @@ export type CompletionCtx = {
6
9
  * Absent for top-level `kind:` — there, no constraint applies. */
7
10
  docKind?: string;
8
11
  yamlPath?: string[];
9
- /** Column where the kind value begins (after `kind:` + whitespace).
10
- * Editor hosts use this to anchor the replace range so completions
11
- * cleanly overwrite a kind that contains `.` (e.g. `Sql.Co|` →
12
- * selecting `Sql.Connection` must replace `Sql.Co`, not just `Co`,
13
- * which VS Code's default word range would). */
14
- valueStartColumn: number;
12
+ /** Full source range of the kind value, so a pick overwrites the whole
13
+ * existing scalar (e.g. `Sql.Co|nnection` + `Sql.Connection` no
14
+ * suffix left behind). */
15
+ replaceRange: ReplaceRange;
15
16
  } | {
16
17
  type: "capability";
17
18
  } | {
@@ -32,48 +33,16 @@ export type CompletionCtx = {
32
33
  /** The kind value of the sibling `kind:` line, if present. */
33
34
  refKind?: string;
34
35
  prefix: string;
35
- valueStartColumn: number;
36
+ replaceRange: ReplaceRange;
36
37
  } | {
37
38
  type: "field-value";
38
39
  docKind: string;
39
40
  field: string;
40
41
  /** Text from the start of the value to the cursor. */
41
42
  prefix: string;
42
- /** 0-based column where the value starts (right after `<field>:` + whitespace). */
43
- valueStartColumn: number;
43
+ /** Full source range of the value being completed. */
44
+ replaceRange: ReplaceRange;
44
45
  };
45
- export declare function findDocBounds(lines: string[], cursorLine: number): {
46
- start: number;
47
- end: number;
48
- };
49
- export declare function extractKindFromDoc(lines: string[], start: number, end: number): string | undefined;
50
- /** Collects every top-level key in the doc bounds, skipping `skipLine` so the
51
- * cursor's own line is treated as "being edited" — its key (if any) stays in
52
- * the suggestion list. Without this the user can't autocomplete an existing
53
- * key from its own line (e.g. `ver|sion:`). */
54
- export declare function extractRootKeys(lines: string[], start: number, end: number, skipLine?: number): Set<string>;
55
- /** Walk backward from cursorLine to build the chain of parent YAML keys.
56
- *
57
- * List-item handling (` - request:` style): the `-` marker sits at the
58
- * line's textual indent, but the key after it (`request`) lives at indent
59
- * `+2`. Whether that post-dash key joins the path depends on the cursor's
60
- * descent:
61
- * - When the cursor's current target indent is GREATER than the post-dash
62
- * key's column, the descent passes through that key (e.g. cursor inside
63
- * `request.method` at indent 6, key `request` at column 4) → push it.
64
- * - When the cursor's current target indent EQUALS the post-dash key's
65
- * column, the post-dash key is a sibling at the list-item level
66
- * (e.g. cursor on `handler:` at indent 4, key `request:` at column 4) →
67
- * skip it; descend straight to the array's parent.
68
- *
69
- * In both cases the next walk step targets `lineIndent` so the `routes:` /
70
- * `steps:` parent of the array is captured. The schema walker auto-descends
71
- * arrays, so no `[]` marker is appended. */
72
- export declare function buildYamlPath(lines: string[], cursorLine: number, docStart: number, cursorIndent: number): string[];
73
- /** Extract sibling keys already present at `indent` within the doc bounds.
74
- * `skipLine` lets the caller exclude the cursor's own line so a key being
75
- * edited (`ver|sion:`) doesn't filter itself out of the suggestion list. */
76
- export declare function extractKeysAtIndent(lines: string[], start: number, end: number, indent: number, skipLine?: number): Set<string>;
77
46
  /** Navigate a JSON Schema hierarchy following `path`, auto-descending into
78
47
  * array items and peeling `anyOf` / `oneOf` branches. When multiple peeled
79
48
  * branches define `properties`, returns a synthetic node whose `properties`
@@ -87,12 +56,16 @@ export declare function navigateSchema(schema: Record<string, any>, path: string
87
56
  * completion to discover what kind of resource the user is targeting in an
88
57
  * object-form ref. Walking stops at the first line with a strictly smaller
89
58
  * indent (that's the parent's structural boundary). */
90
- export declare function findSiblingKindValue(lines: string[], docStart: number, docEnd: number, cursorLine: number, indent: number): string | undefined;
91
59
  /** Looks up the `x-telo-ref` string carried by the schema node at `yamlPath`
92
60
  * inside `definitionSchema`. Checks both the property node directly and its
93
61
  * peeled `anyOf` / `oneOf` branches, since some library schemas place the
94
62
  * annotation at the property level and others inside a branch. Returns
95
63
  * `undefined` when the path doesn't resolve or no ref constraint is declared. */
96
64
  export declare function lookupRefConstraint(definitionSchema: Record<string, any>, yamlPath: string[]): string | undefined;
97
- export declare function detectContext(text: string, line: number, character: number): CompletionCtx | undefined;
65
+ /** Derive a `CompletionCtx` from the AST-resolved cursor (Approach B). The
66
+ * structural resolution lives in `resolveNodeAtPosition`; this only maps a
67
+ * resolved slot onto the completion the editor should offer. `docs` lets a
68
+ * host thread its already-parsed AST; without it we parse locally so this
69
+ * stands alone. */
70
+ export declare function detectContext(text: string, line: number, character: number, docs?: AstDocument[]): CompletionCtx | undefined;
98
71
  //# sourceMappingURL=detect-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"detect-context.d.ts","sourceRoot":"","sources":["../../src/completions/detect-context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"detect-context.d.ts","sourceRoot":"","sources":["../../src/completions/detect-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B,MAAM,MAAM,aAAa,GACrB;IACE,IAAI,EAAE,MAAM,CAAC;IACb;;;uEAGmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;+BAE2B;IAC3B,YAAY,EAAE,YAAY,CAAC;CAC5B,GACD;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GACpF;IACE;;;gDAG4C;IAC5C,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB;6DACyD;IACzD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,YAAY,EAAE,YAAY,CAAC;CAC5B,CAAC;AAuBN;;;;;sCAKsC;AACtC,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,EAAE,GACb,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA4BjC;AAoCD;;;;;wDAKwD;AACxD;;;;kFAIkF;AAClF,wBAAgB,mBAAmB,CACjC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACrC,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,GAAG,SAAS,CAQpB;AAED;;;;oBAIoB;AACpB,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,aAAa,GAAG,SAAS,CAsE3B"}
@@ -1,120 +1,5 @@
1
- export function findDocBounds(lines, cursorLine) {
2
- let start = 0;
3
- for (let i = cursorLine; i >= 0; i--) {
4
- if (lines[i]?.trimEnd() === "---") {
5
- start = i + 1;
6
- break;
7
- }
8
- }
9
- let end = lines.length;
10
- for (let i = cursorLine + 1; i < lines.length; i++) {
11
- if (lines[i]?.trimEnd() === "---") {
12
- end = i;
13
- break;
14
- }
15
- }
16
- return { start, end };
17
- }
18
- export function extractKindFromDoc(lines, start, end) {
19
- for (let i = start; i < end; i++) {
20
- const m = lines[i]?.match(/^kind:\s*(\S+)/);
21
- if (m)
22
- return m[1];
23
- }
24
- return undefined;
25
- }
26
- /** Collects every top-level key in the doc bounds, skipping `skipLine` so the
27
- * cursor's own line is treated as "being edited" — its key (if any) stays in
28
- * the suggestion list. Without this the user can't autocomplete an existing
29
- * key from its own line (e.g. `ver|sion:`). */
30
- export function extractRootKeys(lines, start, end, skipLine) {
31
- const keys = new Set();
32
- for (let i = start; i < end; i++) {
33
- if (i === skipLine)
34
- continue;
35
- const m = lines[i]?.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
36
- if (m)
37
- keys.add(m[1]);
38
- }
39
- return keys;
40
- }
41
- /** Walk backward from cursorLine to build the chain of parent YAML keys.
42
- *
43
- * List-item handling (` - request:` style): the `-` marker sits at the
44
- * line's textual indent, but the key after it (`request`) lives at indent
45
- * `+2`. Whether that post-dash key joins the path depends on the cursor's
46
- * descent:
47
- * - When the cursor's current target indent is GREATER than the post-dash
48
- * key's column, the descent passes through that key (e.g. cursor inside
49
- * `request.method` at indent 6, key `request` at column 4) → push it.
50
- * - When the cursor's current target indent EQUALS the post-dash key's
51
- * column, the post-dash key is a sibling at the list-item level
52
- * (e.g. cursor on `handler:` at indent 4, key `request:` at column 4) →
53
- * skip it; descend straight to the array's parent.
54
- *
55
- * In both cases the next walk step targets `lineIndent` so the `routes:` /
56
- * `steps:` parent of the array is captured. The schema walker auto-descends
57
- * arrays, so no `[]` marker is appended. */
58
- export function buildYamlPath(lines, cursorLine, docStart, cursorIndent) {
59
- if (cursorIndent === 0)
60
- return [];
61
- const path = [];
62
- let targetIndent = cursorIndent;
63
- for (let i = cursorLine - 1; i >= docStart; i--) {
64
- const line = lines[i] ?? "";
65
- const trimmed = line.trimStart();
66
- if (trimmed === "" || trimmed.startsWith("#"))
67
- continue;
68
- const lineIndent = line.length - trimmed.length;
69
- if (lineIndent < targetIndent) {
70
- // Match a plain object key (not a list item marker)
71
- const m = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
72
- if (m) {
73
- path.unshift(m[1]);
74
- targetIndent = lineIndent;
75
- if (lineIndent === 0)
76
- break;
77
- }
78
- else if (trimmed.startsWith("- ")) {
79
- const postDash = trimmed.slice(2);
80
- const km = postDash.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
81
- const keyColumn = lineIndent + 2;
82
- if (km && keyColumn < targetIndent) {
83
- path.unshift(km[1]);
84
- }
85
- targetIndent = lineIndent;
86
- }
87
- else if (trimmed === "-") {
88
- targetIndent = lineIndent;
89
- }
90
- else {
91
- // Hit something we can't parse; stop
92
- break;
93
- }
94
- }
95
- }
96
- return path;
97
- }
98
- /** Extract sibling keys already present at `indent` within the doc bounds.
99
- * `skipLine` lets the caller exclude the cursor's own line so a key being
100
- * edited (`ver|sion:`) doesn't filter itself out of the suggestion list. */
101
- export function extractKeysAtIndent(lines, start, end, indent, skipLine) {
102
- const keys = new Set();
103
- const prefix = " ".repeat(indent);
104
- for (let i = start; i < end; i++) {
105
- if (i === skipLine)
106
- continue;
107
- const line = lines[i] ?? "";
108
- if (!line.startsWith(prefix))
109
- continue;
110
- const rest = line.slice(indent);
111
- const m = rest.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
112
- if (m && line.length - line.trimStart().length === indent) {
113
- keys.add(m[1]);
114
- }
115
- }
116
- return keys;
117
- }
1
+ import { parseToAst } from "@telorun/analyzer";
2
+ import { resolveNodeAtPosition } from "./resolve-node.js";
118
3
  /** Returns every schema branch reachable from `node` after peeling `anyOf` /
119
4
  * `oneOf` recursively. A branch with no combinators is its own only entry.
120
5
  * Used so an `x-telo-ref` slot like `{anyOf: [{type: string}, {type: object,
@@ -212,36 +97,6 @@ function unionLeaves(parent, leaves) {
212
97
  * completion to discover what kind of resource the user is targeting in an
213
98
  * object-form ref. Walking stops at the first line with a strictly smaller
214
99
  * indent (that's the parent's structural boundary). */
215
- export function findSiblingKindValue(lines, docStart, docEnd, cursorLine, indent) {
216
- const prefix = " ".repeat(indent);
217
- const scan = (range) => {
218
- for (const i of range) {
219
- const line = lines[i] ?? "";
220
- if (line.trim() === "" || line.trim().startsWith("#"))
221
- continue;
222
- if (line.trimEnd() === "---")
223
- return undefined;
224
- const lineIndent = line.length - line.trimStart().length;
225
- if (lineIndent < indent)
226
- return undefined; // parent boundary
227
- if (lineIndent !== indent || !line.startsWith(prefix))
228
- continue;
229
- const m = line.slice(indent).match(/^kind:\s*(\S+)/);
230
- if (m)
231
- return m[1];
232
- }
233
- return undefined;
234
- };
235
- // Forward then backward — order doesn't matter for correctness because
236
- // any sibling kind value at this indent applies to the same object.
237
- const after = [];
238
- for (let i = cursorLine + 1; i < docEnd; i++)
239
- after.push(i);
240
- const before = [];
241
- for (let i = cursorLine - 1; i >= docStart; i--)
242
- before.push(i);
243
- return scan(after) ?? scan(before);
244
- }
245
100
  /** Looks up the `x-telo-ref` string carried by the schema node at `yamlPath`
246
101
  * inside `definitionSchema`. Checks both the property node directly and its
247
102
  * peeled `anyOf` / `oneOf` branches, since some library schemas place the
@@ -259,129 +114,75 @@ export function lookupRefConstraint(definitionSchema, yamlPath) {
259
114
  }
260
115
  return undefined;
261
116
  }
262
- export function detectContext(text, line, character) {
263
- const lines = text.split("\n");
264
- const currentLine = lines[line] ?? "";
265
- const { start, end } = findDocBounds(lines, line);
266
- const docKind = extractKindFromDoc(lines, start, end);
267
- // Kind value completion fires ONLY when the cursor sits past the `:` of a
268
- // `kind:` line. With the cursor on the key portion (start, middle, or right
269
- // before the colon) we fall through to prop-key completion so `kind` itself
270
- // can be suggested. Matches both top-level (`kind: …`) and indented forms;
271
- // indented form also surfaces the enclosing ref slot for filtering.
272
- const kindLineMatch = currentLine.match(/^(\s*)kind:(\s*)(\S*)$/);
273
- if (kindLineMatch) {
274
- const indent = kindLineMatch[1].length;
275
- const valueStart = indent + "kind:".length + kindLineMatch[2].length;
276
- if (character >= valueStart) {
277
- if (indent === 0)
278
- return { type: "kind", valueStartColumn: valueStart };
279
- if (docKind) {
280
- const yamlPath = buildYamlPath(lines, line, start, indent);
281
- return { type: "kind", docKind, yamlPath, valueStartColumn: valueStart };
282
- }
117
+ /** Derive a `CompletionCtx` from the AST-resolved cursor (Approach B). The
118
+ * structural resolution lives in `resolveNodeAtPosition`; this only maps a
119
+ * resolved slot onto the completion the editor should offer. `docs` lets a
120
+ * host thread its already-parsed AST; without it we parse locally so this
121
+ * stands alone. */
122
+ export function detectContext(text, line, character, docs) {
123
+ const resolved = resolveNodeAtPosition(text, docs ?? parseToAst(text), line, character);
124
+ if (!resolved)
125
+ return undefined;
126
+ const { docKind } = resolved;
127
+ if (resolved.slot === "value") {
128
+ // Inside a CEL body — structural completion does not apply (a future
129
+ // CEL-completion feature consumes `resolved.cel`).
130
+ if (resolved.cel)
131
+ return undefined;
132
+ const replaceRange = resolved.replaceRange;
133
+ if (!replaceRange)
134
+ return undefined;
135
+ const key = resolved.path[resolved.path.length - 1];
136
+ const parentPath = resolved.path.slice(0, -1);
137
+ const prefix = resolved.prefix ?? "";
138
+ if (key === "kind") {
139
+ if (parentPath.length === 0)
140
+ return { type: "kind", replaceRange };
141
+ if (docKind)
142
+ return { type: "kind", docKind, yamlPath: parentPath, replaceRange };
143
+ return undefined;
283
144
  }
284
- // Cursor is on the key portion fall through to prop-key handling.
285
- }
286
- // Capability value completion: only inside Telo.Definition docs
287
- if (/^capability:\s*\S*$/.test(currentLine) && docKind === "Telo.Definition") {
288
- return { type: "capability" };
289
- }
290
- // Ref-name value completion: cursor on the value of a `name:` line inside
291
- // an object-form ref (sibling `kind:` declares which resource kind we're
292
- // referencing). The enclosing parent slot's schema carries the ref
293
- // constraint, but we don't need to consult it here — `buildCompletions`
294
- // will fall back to filtering by `refKind` regardless of the schema. Doing
295
- // so keeps editor autocomplete working even when the registry hasn't fully
296
- // resolved the resource's definition.
297
- const nameLineMatch = currentLine.match(/^(\s+)name:(\s*)(\S*)$/);
298
- if (nameLineMatch && docKind) {
299
- const indent = nameLineMatch[1].length;
300
- const valueStart = indent + "name:".length + nameLineMatch[2].length;
301
- const valuePrefix = nameLineMatch[3];
302
- if (character >= valueStart) {
303
- const yamlPath = buildYamlPath(lines, line, start, indent);
304
- // The yamlPath built here points at the parent slot — for
305
- // `connection: { kind: …, name: | }` the path is `["connection"]`.
306
- // The sibling `kind:` lives at the same indent as our `name:`, so we
307
- // scan the doc bounds for it.
308
- const refKind = findSiblingKindValue(lines, start, end, line, indent);
145
+ if (key === "capability" && docKind === "Telo.Definition") {
146
+ return { type: "capability" };
147
+ }
148
+ // Any `name:` value is a candidate ref target; the sibling `kind:` (when
149
+ // present) narrows the in-file resource list. Harmless on `metadata.name`,
150
+ // where no ref constraint resolves and the list is the fallback.
151
+ if (key === "name" && docKind) {
309
152
  return {
310
153
  type: "ref-name",
311
154
  docKind,
312
- yamlPath,
313
- refKind,
314
- prefix: valuePrefix,
315
- valueStartColumn: valueStart,
155
+ yamlPath: parentPath,
156
+ refKind: resolved.siblingKind,
157
+ prefix,
158
+ replaceRange,
316
159
  };
317
160
  }
318
- }
319
- if (!docKind)
320
- return undefined;
321
- // Import-source value completion: entries in the `imports:` map on a module
322
- // doc. Two shapes are completed against filesystem paths / registry ids:
323
- // scalar shorthand ` Alias: <src>` → the entry value IS the source
324
- // object form ` source: <src>` → the `source:` under `imports.<Alias>`
325
- // Gated on the enclosing path resolving to the `imports:` map so unrelated
326
- // `source:` fields (e.g. `Assert.Manifest.source`) never trigger it.
327
- if (docKind === "Telo.Application" || docKind === "Telo.Library") {
328
- // Require a space after the colon (`key: …`) so a bare object-form header —
329
- // ` Tiny:` about to carry a nested `source:`/`variables:` — is treated as a
330
- // key position, not an import-source value. A flow-map (`Alias: { … }`) never
331
- // matches: `\S*` can't span the spaces inside the braces.
332
- const entryMatch = currentLine.match(/^(\s+)([A-Za-z_][\w-]*):(\s+)(\S*)$/);
333
- if (entryMatch) {
334
- const indent = entryMatch[1].length;
335
- const key = entryMatch[2];
336
- const valueStartColumn = indent + key.length + 1 + entryMatch[3].length;
337
- if (character >= valueStartColumn) {
338
- const parentPath = buildYamlPath(lines, line, start, indent);
339
- const isScalarEntry = parentPath.length === 1 && parentPath[0] === "imports";
340
- const isObjectSource = key === "source" && parentPath.length === 2 && parentPath[0] === "imports";
341
- if (isScalarEntry || isObjectSource) {
342
- const prefix = currentLine.slice(valueStartColumn, character);
343
- return { type: "field-value", docKind, field: "import-source", prefix, valueStartColumn };
344
- }
161
+ // Import-source: the scalar shorthand `imports.<Alias>` or the object-form
162
+ // `imports.<Alias>.source`. `spaceAfterColon` distinguishes `Console: ` (a
163
+ // value) from a bare `Tiny:` header about to carry a nested `source:`.
164
+ if ((docKind === "Telo.Application" || docKind === "Telo.Library") &&
165
+ resolved.spaceAfterColon) {
166
+ const isScalarEntry = parentPath.length === 1 && parentPath[0] === "imports";
167
+ const isObjectSource = key === "source" && parentPath.length === 2 && parentPath[0] === "imports";
168
+ if (isScalarEntry || isObjectSource) {
169
+ return { type: "field-value", docKind, field: "import-source", prefix, replaceRange };
345
170
  }
346
171
  }
347
- }
348
- const trimmed = currentLine.trim();
349
- // Trigger when the cursor is on the KEY portion of the line. Three cases:
350
- // 1. Blank line / whitespace only — `existingKeys` skip means the user is
351
- // starting a fresh key.
352
- // 2. Line has no `:` yet (e.g. `vers`, `version`) — partial key being typed.
353
- // 3. Line has `key: value` and the cursor is at or before the colon.
354
- // The line text is preserved so `version|: 1.0.0` keeps suggesting keys
355
- // while `version: |1.0.0` falls through to no completion.
356
- const colonIdx = currentLine.indexOf(":");
357
- const beforeColon = colonIdx === -1 ? currentLine : currentLine.slice(0, colonIdx);
358
- const isKeyLine = trimmed === "" ||
359
- (colonIdx === -1 && /^\s*[a-zA-Z_][a-zA-Z0-9_]*$/.test(currentLine)) ||
360
- (colonIdx !== -1 &&
361
- character <= colonIdx &&
362
- /^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*$/.test(beforeColon));
363
- if (!isKeyLine)
364
172
  return undefined;
365
- // Indent resolution: for a whitespace-only line the cursor's column tells
366
- // us exactly where the user is about to type — VS Code parks the cursor at
367
- // the new auto-indent after Enter, and any deviation (backspace to col 0,
368
- // type extra spaces) is intentional. Trusting `character` also lets the
369
- // user reach root level (col 0) on a trailing blank line even when the
370
- // previous non-blank line was nested.
371
- const indent = trimmed === ""
372
- ? character
373
- : currentLine.length - currentLine.trimStart().length;
374
- if (indent === 0) {
375
- return {
376
- type: "prop-key",
377
- docKind,
378
- yamlPath: [],
379
- existingKeys: extractRootKeys(lines, start, end, line),
380
- };
381
173
  }
382
- const yamlPath = buildYamlPath(lines, line, start, indent);
383
- if (yamlPath.length === 0)
384
- return undefined; // couldn't resolve parentbail
385
- const existingKeys = extractKeysAtIndent(lines, start, end, indent, line);
386
- return { type: "prop-key", docKind, yamlPath, existingKeys };
174
+ // Key position (existing key, blank line, or trailing indent). Complete
175
+ // against the nearest enclosing inline resource's schema (or the root
176
+ // resource), with the path made relative to it so a prop key inside
177
+ // `mount: { kind: Crud.Resource, }` offers Crud.Resource's fields, not the
178
+ // outer ref slot's.
179
+ const scopeKind = resolved.resourceKind ?? docKind;
180
+ if (!scopeKind)
181
+ return undefined;
182
+ return {
183
+ type: "prop-key",
184
+ docKind: scopeKind,
185
+ yamlPath: resolved.path.slice(resolved.resourceDepth ?? 0),
186
+ existingKeys: resolved.existingKeys ?? new Set(),
187
+ };
387
188
  }