@telorun/ide-support 0.4.1 → 0.4.4

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.
@@ -23,16 +23,38 @@ export function extractKindFromDoc(lines, start, end) {
23
23
  }
24
24
  return undefined;
25
25
  }
26
- export function extractRootKeys(lines, start, end) {
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) {
27
31
  const keys = new Set();
28
32
  for (let i = start; i < end; i++) {
33
+ if (i === skipLine)
34
+ continue;
29
35
  const m = lines[i]?.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
30
36
  if (m)
31
37
  keys.add(m[1]);
32
38
  }
33
39
  return keys;
34
40
  }
35
- /** Walk backward from cursorLine to build the chain of parent YAML keys. */
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. */
36
58
  export function buildYamlPath(lines, cursorLine, docStart, cursorIndent) {
37
59
  if (cursorIndent === 0)
38
60
  return [];
@@ -53,19 +75,35 @@ export function buildYamlPath(lines, cursorLine, docStart, cursorIndent) {
53
75
  if (lineIndent === 0)
54
76
  break;
55
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
+ }
56
90
  else {
57
- // Hit something we can't parse (e.g. a list item `- ...`); stop
91
+ // Hit something we can't parse; stop
58
92
  break;
59
93
  }
60
94
  }
61
95
  }
62
96
  return path;
63
97
  }
64
- /** Extract sibling keys already present at `indent` within the doc bounds. */
65
- export function extractKeysAtIndent(lines, start, end, indent) {
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) {
66
102
  const keys = new Set();
67
103
  const prefix = " ".repeat(indent);
68
104
  for (let i = start; i < end; i++) {
105
+ if (i === skipLine)
106
+ continue;
69
107
  const line = lines[i] ?? "";
70
108
  if (!line.startsWith(prefix))
71
109
  continue;
@@ -77,62 +115,207 @@ export function extractKeysAtIndent(lines, start, end, indent) {
77
115
  }
78
116
  return keys;
79
117
  }
80
- /** Navigate a JSON Schema hierarchy following `path`, auto-descending into array items. */
118
+ /** Returns every schema branch reachable from `node` after peeling `anyOf` /
119
+ * `oneOf` recursively. A branch with no combinators is its own only entry.
120
+ * Used so an `x-telo-ref` slot like `{anyOf: [{type: string}, {type: object,
121
+ * properties: …}]}` exposes the object branch's properties to completion. */
122
+ function peelCombinators(node) {
123
+ const out = [];
124
+ const visit = (n) => {
125
+ if (!n || typeof n !== "object")
126
+ return;
127
+ const branches = [];
128
+ if (Array.isArray(n.anyOf))
129
+ branches.push(...n.anyOf);
130
+ if (Array.isArray(n.oneOf))
131
+ branches.push(...n.oneOf);
132
+ if (branches.length === 0) {
133
+ out.push(n);
134
+ return;
135
+ }
136
+ for (const b of branches)
137
+ visit(b);
138
+ };
139
+ visit(node);
140
+ return out;
141
+ }
142
+ /** Navigate a JSON Schema hierarchy following `path`, auto-descending into
143
+ * array items and peeling `anyOf` / `oneOf` branches. When multiple peeled
144
+ * branches define `properties`, returns a synthetic node whose `properties`
145
+ * is the union (first-wins on key collision) and whose `required` is the
146
+ * intersection — enough for propKeyCompletions to surface every key a value
147
+ * at this slot can legally carry. */
81
148
  export function navigateSchema(schema, path) {
82
149
  let current = schema;
83
150
  for (const segment of path) {
84
- // Auto-descend through arrays before looking up the next key
85
- while (current.type === "array" && current.items) {
86
- current = current.items;
151
+ const candidates = peelCombinators(current).flatMap((node) => {
152
+ const expanded = [];
153
+ let cur = node;
154
+ while (cur.type === "array" && cur.items)
155
+ cur = cur.items;
156
+ for (const peeled of peelCombinators(cur))
157
+ expanded.push(peeled);
158
+ return expanded;
159
+ });
160
+ let next;
161
+ for (const cand of candidates) {
162
+ const sub = cand.properties?.[segment];
163
+ if (sub) {
164
+ next = sub;
165
+ break;
166
+ }
87
167
  }
88
- const props = current.properties;
89
- if (!props?.[segment])
168
+ if (!next)
90
169
  return undefined;
91
- current = props[segment];
170
+ current = next;
92
171
  }
93
172
  // Auto-descend through a trailing array at the leaf (e.g. cursor inside `mounts:` items)
94
173
  while (current.type === "array" && current.items) {
95
174
  current = current.items;
96
175
  }
97
- return current;
176
+ const leaves = peelCombinators(current);
177
+ if (leaves.length === 1)
178
+ return leaves[0];
179
+ return unionLeaves(current, leaves);
98
180
  }
99
- /**
100
- * For blank lines (and lines with only whitespace), infer the intended indent
101
- * from context rather than relying on the cursor column, which is often 0
102
- * even when the cursor is semantically inside a nested block.
103
- *
104
- * Strategy: look at the previous non-empty line.
105
- * - If it ends with `:` (bare object key, no value) → cursor is one level deeper.
106
- * - Otherwise → cursor is a sibling of that key (same indent).
107
- */
108
- export function inferIndentForBlankLine(lines, cursorLine, docStart) {
109
- for (let i = cursorLine - 1; i >= docStart; i--) {
110
- const line = lines[i] ?? "";
111
- if (line.trim() === "" || line.trim().startsWith("#"))
112
- continue;
113
- if (line.trimEnd() === "---")
114
- break;
115
- const lineIndent = line.length - line.trimStart().length;
116
- if (line.trimEnd().endsWith(":")) {
117
- return lineIndent + 2;
181
+ /** Merge multiple peeled schema branches into one node for completion purposes.
182
+ * Property maps are unioned (first branch wins on key collision). `required`
183
+ * becomes the intersection so optional-in-any-branch keys still surface.
184
+ * `x-telo-ref` from the unpeeled parent is preserved so ref-aware lookups
185
+ * (`lookupRefConstraint`) still see the constraint when navigateSchema is
186
+ * called on a property that places the annotation alongside `anyOf`/`oneOf`. */
187
+ function unionLeaves(parent, leaves) {
188
+ const properties = {};
189
+ const requiredSets = [];
190
+ for (const leaf of leaves) {
191
+ const props = leaf.properties;
192
+ if (props) {
193
+ for (const [k, v] of Object.entries(props)) {
194
+ if (!(k in properties))
195
+ properties[k] = v;
196
+ }
197
+ }
198
+ requiredSets.push(new Set(Array.isArray(leaf.required) ? leaf.required : []));
199
+ }
200
+ let required = [];
201
+ if (requiredSets.length > 0) {
202
+ required = [...requiredSets[0]].filter((k) => requiredSets.every((s) => s.has(k)));
203
+ }
204
+ const out = { type: "object", properties, required };
205
+ if (typeof parent["x-telo-ref"] === "string")
206
+ out["x-telo-ref"] = parent["x-telo-ref"];
207
+ return out;
208
+ }
209
+ /** Walks up and down from `cursorLine` looking for a sibling line at the
210
+ * exact same indent whose key is `kind`. The value of the first such line
211
+ * is returned (alias form, e.g. `"Sql.Connection"`). Used by ref-name
212
+ * completion to discover what kind of resource the user is targeting in an
213
+ * object-form ref. Walking stops at the first line with a strictly smaller
214
+ * 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];
118
232
  }
119
- return lineIndent;
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
+ /** Looks up the `x-telo-ref` string carried by the schema node at `yamlPath`
246
+ * inside `definitionSchema`. Checks both the property node directly and its
247
+ * peeled `anyOf` / `oneOf` branches, since some library schemas place the
248
+ * annotation at the property level and others inside a branch. Returns
249
+ * `undefined` when the path doesn't resolve or no ref constraint is declared. */
250
+ export function lookupRefConstraint(definitionSchema, yamlPath) {
251
+ const node = navigateSchema(definitionSchema, yamlPath);
252
+ if (!node)
253
+ return undefined;
254
+ if (typeof node["x-telo-ref"] === "string")
255
+ return node["x-telo-ref"];
256
+ for (const branch of peelCombinators(node)) {
257
+ if (typeof branch["x-telo-ref"] === "string")
258
+ return branch["x-telo-ref"];
120
259
  }
121
- return 0;
260
+ return undefined;
122
261
  }
123
262
  export function detectContext(text, line, character) {
124
263
  const lines = text.split("\n");
125
264
  const currentLine = lines[line] ?? "";
126
- // Kind value completion: `kind: ` or `kind: SomePrefix`
127
- if (/^kind:\s*\S*$/.test(currentLine)) {
128
- return { type: "kind" };
129
- }
130
265
  const { start, end } = findDocBounds(lines, line);
131
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
+ }
283
+ }
284
+ // Cursor is on the key portion — fall through to prop-key handling.
285
+ }
132
286
  // Capability value completion: only inside Telo.Definition docs
133
287
  if (/^capability:\s*\S*$/.test(currentLine) && docKind === "Telo.Definition") {
134
288
  return { type: "capability" };
135
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);
309
+ return {
310
+ type: "ref-name",
311
+ docKind,
312
+ yamlPath,
313
+ refKind,
314
+ prefix: valuePrefix,
315
+ valueStartColumn: valueStart,
316
+ };
317
+ }
318
+ }
136
319
  if (!docKind)
137
320
  return undefined;
138
321
  // Field-value completion: `source: <prefix>` on a top-level Telo.Import field.
@@ -149,19 +332,42 @@ export function detectContext(text, line, character) {
149
332
  }
150
333
  }
151
334
  const trimmed = currentLine.trim();
152
- // Only trigger when the line looks like a key being typed (or is blank)
153
- const isKeyLine = trimmed === "" || /^[a-zA-Z_][a-zA-Z0-9_]*:?$/.test(trimmed);
335
+ // Trigger when the cursor is on the KEY portion of the line. Three cases:
336
+ // 1. Blank line / whitespace only `existingKeys` skip means the user is
337
+ // starting a fresh key.
338
+ // 2. Line has no `:` yet (e.g. `vers`, `version`) — partial key being typed.
339
+ // 3. Line has `key: value` and the cursor is at or before the colon.
340
+ // The line text is preserved so `version|: 1.0.0` keeps suggesting keys
341
+ // while `version: |1.0.0` falls through to no completion.
342
+ const colonIdx = currentLine.indexOf(":");
343
+ const beforeColon = colonIdx === -1 ? currentLine : currentLine.slice(0, colonIdx);
344
+ const isKeyLine = trimmed === "" ||
345
+ (colonIdx === -1 && /^\s*[a-zA-Z_][a-zA-Z0-9_]*$/.test(currentLine)) ||
346
+ (colonIdx !== -1 &&
347
+ character <= colonIdx &&
348
+ /^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*$/.test(beforeColon));
154
349
  if (!isKeyLine)
155
350
  return undefined;
351
+ // Indent resolution: for a whitespace-only line the cursor's column tells
352
+ // us exactly where the user is about to type — VS Code parks the cursor at
353
+ // the new auto-indent after Enter, and any deviation (backspace to col 0,
354
+ // type extra spaces) is intentional. Trusting `character` also lets the
355
+ // user reach root level (col 0) on a trailing blank line even when the
356
+ // previous non-blank line was nested.
156
357
  const indent = trimmed === ""
157
- ? inferIndentForBlankLine(lines, line, start)
358
+ ? character
158
359
  : currentLine.length - currentLine.trimStart().length;
159
360
  if (indent === 0) {
160
- return { type: "prop-key", docKind, yamlPath: [], existingKeys: extractRootKeys(lines, start, end) };
361
+ return {
362
+ type: "prop-key",
363
+ docKind,
364
+ yamlPath: [],
365
+ existingKeys: extractRootKeys(lines, start, end, line),
366
+ };
161
367
  }
162
368
  const yamlPath = buildYamlPath(lines, line, start, indent);
163
369
  if (yamlPath.length === 0)
164
370
  return undefined; // couldn't resolve parent — bail
165
- const existingKeys = extractKeysAtIndent(lines, start, end, indent);
371
+ const existingKeys = extractKeysAtIndent(lines, start, end, indent, line);
166
372
  return { type: "prop-key", docKind, yamlPath, existingKeys };
167
373
  }
@@ -1 +1 @@
1
- {"version":3,"file":"prop-keys.d.ts","sourceRoot":"","sources":["../../src/completions/prop-keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAAE,EAClB,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,EACzB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,GACrC,gBAAgB,EAAE,CA+CpB"}
1
+ {"version":3,"file":"prop-keys.d.ts","sourceRoot":"","sources":["../../src/completions/prop-keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAiBpD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAAE,EAClB,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,EACzB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,GACrC,gBAAgB,EAAE,CAkCpB"}
@@ -1,22 +1,50 @@
1
1
  import { navigateSchema } from "./detect-context.js";
2
+ /** Kernel-implicit fields. Every Telo resource declares its `kind` and a
3
+ * `metadata` object; the analyzer's schema validator injects them when
4
+ * the definition uses `additionalProperties: false`. Completion has the
5
+ * same need — domain-specific schemas (`Http.Api`, `Sql.Query`, …) don't
6
+ * enumerate these in their own `properties`, so without an explicit fallback
7
+ * the user can't autocomplete `kind:` or `metadata:` on those resources. */
8
+ const ROOT_IMPLICIT_PROPS = {
9
+ kind: { type: "string", description: "The fully-qualified resource kind." },
10
+ metadata: {
11
+ type: "object",
12
+ description: "Resource metadata (name, namespace, version).",
13
+ },
14
+ };
2
15
  export function propKeyCompletions(kind, yamlPath, existingKeys, registry) {
3
16
  if (!registry)
4
17
  return [];
5
18
  const definition = registry.resolveDefinition(kind);
6
- if (!definition?.schema)
19
+ if (!definition?.schema) {
20
+ // Unknown kind (often: an unloaded import). At root level, still surface
21
+ // the universal `kind` / `metadata` keys so completion isn't dead when
22
+ // the registry hasn't resolved the resource type yet.
23
+ if (yamlPath.length === 0) {
24
+ return buildItems(ROOT_IMPLICIT_PROPS, existingKeys, new Set());
25
+ }
7
26
  return [];
27
+ }
8
28
  const targetSchema = yamlPath.length === 0
9
29
  ? definition.schema
10
30
  : navigateSchema(definition.schema, yamlPath);
11
- if (!targetSchema?.properties)
31
+ if (!targetSchema?.properties) {
32
+ if (yamlPath.length === 0) {
33
+ return buildItems(ROOT_IMPLICIT_PROPS, existingKeys, new Set());
34
+ }
12
35
  return [];
36
+ }
13
37
  const required = new Set(Array.isArray(targetSchema.required) ? targetSchema.required : []);
38
+ const properties = yamlPath.length === 0
39
+ ? { ...ROOT_IMPLICIT_PROPS, ...targetSchema.properties }
40
+ : targetSchema.properties;
41
+ return buildItems(properties, existingKeys, required);
42
+ }
43
+ function buildItems(properties, existingKeys, required) {
14
44
  const items = [];
15
- for (const [prop, propSchema] of Object.entries(targetSchema.properties)) {
45
+ for (const [prop, propSchema] of Object.entries(properties)) {
16
46
  if (existingKeys.has(prop))
17
47
  continue;
18
- if (yamlPath.length === 0 && (prop === "kind" || prop === "metadata"))
19
- continue;
20
48
  const item = {
21
49
  label: prop,
22
50
  kind: "property",
@@ -3,8 +3,14 @@ import type { DiagnosticContext } from "../types.js";
3
3
  /** Falls back through the chain from the VS Code extension's inline resolver
4
4
  * (ide/vscode/src/extension.ts:203-216 before this package existed):
5
5
  * 1. `d.range` if present.
6
- * 2. `positionIndex.get(d.data.path)` when both are available.
7
- * 3. Whole-line span at `sourceLine` when known.
8
- * 4. `(0,0)-(0,0)` as a last resort. Never undefined. */
6
+ * 2. `positionIndex.get(d.data.path)` for a direct hit (covers diagnostics
7
+ * that target an existing value, e.g. wrong type, enum violation).
8
+ * 3. If the leaf is missing (e.g. "missing required property" — `.type`
9
+ * isn't in the YAML yet), walk one segment up at a time and squiggle
10
+ * just the parent's key identifier (`@key:<parent>`), not the parent's
11
+ * full value block. Keeps the squiggle scoped to the incomplete entry
12
+ * instead of spreading across every line of the surrounding map.
13
+ * 4. Whole-line span at `sourceLine` when known.
14
+ * 5. `(0,0)-(0,0)` as a last resort. Never undefined. */
9
15
  export declare function resolveRange(d: AnalysisDiagnostic, ctx: DiagnosticContext): Range;
10
16
  //# sourceMappingURL=range-resolver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"range-resolver.d.ts","sourceRoot":"","sources":["../../src/diagnostics/range-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOrD;;;;;4DAK4D;AAC5D,wBAAgB,YAAY,CAAC,CAAC,EAAE,kBAAkB,EAAE,GAAG,EAAE,iBAAiB,GAAG,KAAK,CAiBjF"}
1
+ {"version":3,"file":"range-resolver.d.ts","sourceRoot":"","sources":["../../src/diagnostics/range-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOrD;;;;;;;;;;;4DAW4D;AAC5D,wBAAgB,YAAY,CAAC,CAAC,EAAE,kBAAkB,EAAE,GAAG,EAAE,iBAAiB,GAAG,KAAK,CAuBjF"}
@@ -5,17 +5,31 @@ const ZERO_RANGE = {
5
5
  /** Falls back through the chain from the VS Code extension's inline resolver
6
6
  * (ide/vscode/src/extension.ts:203-216 before this package existed):
7
7
  * 1. `d.range` if present.
8
- * 2. `positionIndex.get(d.data.path)` when both are available.
9
- * 3. Whole-line span at `sourceLine` when known.
10
- * 4. `(0,0)-(0,0)` as a last resort. Never undefined. */
8
+ * 2. `positionIndex.get(d.data.path)` for a direct hit (covers diagnostics
9
+ * that target an existing value, e.g. wrong type, enum violation).
10
+ * 3. If the leaf is missing (e.g. "missing required property" — `.type`
11
+ * isn't in the YAML yet), walk one segment up at a time and squiggle
12
+ * just the parent's key identifier (`@key:<parent>`), not the parent's
13
+ * full value block. Keeps the squiggle scoped to the incomplete entry
14
+ * instead of spreading across every line of the surrounding map.
15
+ * 4. Whole-line span at `sourceLine` when known.
16
+ * 5. `(0,0)-(0,0)` as a last resort. Never undefined. */
11
17
  export function resolveRange(d, ctx) {
12
18
  if (d.range)
13
19
  return d.range;
14
20
  const fieldPath = d.data?.path;
15
21
  if (fieldPath !== undefined && ctx.positionIndex) {
16
- const fieldRange = ctx.positionIndex.get(fieldPath);
17
- if (fieldRange)
18
- return fieldRange;
22
+ const direct = ctx.positionIndex.get(fieldPath);
23
+ if (direct)
24
+ return direct;
25
+ for (const parent of parentPaths(fieldPath).slice(1)) {
26
+ const keyRange = ctx.positionIndex.get(`@key:${parent}`);
27
+ if (keyRange)
28
+ return keyRange;
29
+ const valueRange = ctx.positionIndex.get(parent);
30
+ if (valueRange)
31
+ return valueRange;
32
+ }
19
33
  }
20
34
  if (ctx.sourceLine !== undefined) {
21
35
  return {
@@ -25,3 +39,22 @@ export function resolveRange(d, ctx) {
25
39
  }
26
40
  return ZERO_RANGE;
27
41
  }
42
+ /** Yield `path`, then progressively shorter parents formed by stripping
43
+ * trailing dotted segments and array index suffixes. For
44
+ * `"secrets.openaiApiKey.type"` → `["secrets.openaiApiKey.type",
45
+ * "secrets.openaiApiKey", "secrets"]`. For `"routes[0].handler"` →
46
+ * `["routes[0].handler", "routes[0]", "routes"]`. */
47
+ function parentPaths(path) {
48
+ const out = [];
49
+ let cur = path;
50
+ while (cur.length > 0) {
51
+ out.push(cur);
52
+ const lastDot = cur.lastIndexOf(".");
53
+ const lastBracket = cur.lastIndexOf("[");
54
+ const cut = Math.max(lastDot, lastBracket);
55
+ if (cut <= 0)
56
+ break;
57
+ cur = cur.slice(0, cut);
58
+ }
59
+ return out;
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/ide-support",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "Editor-host-agnostic IDE support (completions, diagnostic normalization) for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -36,13 +36,16 @@
36
36
  "src/**"
37
37
  ],
38
38
  "dependencies": {
39
- "@telorun/analyzer": "0.10.1"
39
+ "@telorun/analyzer": "1.1.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^20.0.0",
43
- "typescript": "^5.0.0"
43
+ "typescript": "^5.0.0",
44
+ "vitest": "^2.1.8"
44
45
  },
45
46
  "scripts": {
46
- "build": "tsc -p tsconfig.lib.json"
47
+ "build": "tsc -p tsconfig.lib.json",
48
+ "test": "vitest run",
49
+ "test:watch": "vitest"
47
50
  }
48
51
  }