@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.
- package/README.md +3 -3
- package/dist/completions/build.d.ts.map +1 -1
- package/dist/completions/build.js +141 -12
- package/dist/completions/detect-context.d.ts +71 -15
- package/dist/completions/detect-context.d.ts.map +1 -1
- package/dist/completions/detect-context.js +249 -43
- package/dist/completions/prop-keys.d.ts.map +1 -1
- package/dist/completions/prop-keys.js +33 -5
- package/dist/diagnostics/range-resolver.d.ts +9 -3
- package/dist/diagnostics/range-resolver.d.ts.map +1 -1
- package/dist/diagnostics/range-resolver.js +39 -6
- package/package.json +7 -4
- package/src/completions/build.ts +166 -12
- package/src/completions/detect-context.ts +283 -43
- package/src/completions/prop-keys.ts +43 -7
- package/src/diagnostics/range-resolver.ts +36 -5
|
@@ -23,16 +23,38 @@ export function extractKindFromDoc(lines, start, end) {
|
|
|
23
23
|
}
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
if (!props?.[segment])
|
|
168
|
+
if (!next)
|
|
90
169
|
return undefined;
|
|
91
|
-
current =
|
|
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
|
-
|
|
176
|
+
const leaves = peelCombinators(current);
|
|
177
|
+
if (leaves.length === 1)
|
|
178
|
+
return leaves[0];
|
|
179
|
+
return unionLeaves(current, leaves);
|
|
98
180
|
}
|
|
99
|
-
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
153
|
-
|
|
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
|
-
?
|
|
358
|
+
? character
|
|
158
359
|
: currentLine.length - currentLine.trimStart().length;
|
|
159
360
|
if (indent === 0) {
|
|
160
|
-
return {
|
|
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;
|
|
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(
|
|
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)`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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)`
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
17
|
-
if (
|
|
18
|
-
return
|
|
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.
|
|
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": "
|
|
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
|
}
|