@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.
@@ -2,6 +2,20 @@ import type { AnalysisRegistry } from "@telorun/analyzer";
2
2
  import type { CompletionResult } from "../types.js";
3
3
  import { navigateSchema } from "./detect-context.js";
4
4
 
5
+ /** Kernel-implicit fields. Every Telo resource declares its `kind` and a
6
+ * `metadata` object; the analyzer's schema validator injects them when
7
+ * the definition uses `additionalProperties: false`. Completion has the
8
+ * same need — domain-specific schemas (`Http.Api`, `Sql.Query`, …) don't
9
+ * enumerate these in their own `properties`, so without an explicit fallback
10
+ * the user can't autocomplete `kind:` or `metadata:` on those resources. */
11
+ const ROOT_IMPLICIT_PROPS: Record<string, Record<string, any>> = {
12
+ kind: { type: "string", description: "The fully-qualified resource kind." },
13
+ metadata: {
14
+ type: "object",
15
+ description: "Resource metadata (name, namespace, version).",
16
+ },
17
+ };
18
+
5
19
  export function propKeyCompletions(
6
20
  kind: string,
7
21
  yamlPath: string[],
@@ -11,24 +25,46 @@ export function propKeyCompletions(
11
25
  if (!registry) return [];
12
26
 
13
27
  const definition = registry.resolveDefinition(kind);
14
- if (!definition?.schema) return [];
28
+ if (!definition?.schema) {
29
+ // Unknown kind (often: an unloaded import). At root level, still surface
30
+ // the universal `kind` / `metadata` keys so completion isn't dead when
31
+ // the registry hasn't resolved the resource type yet.
32
+ if (yamlPath.length === 0) {
33
+ return buildItems(ROOT_IMPLICIT_PROPS, existingKeys, new Set<string>());
34
+ }
35
+ return [];
36
+ }
15
37
 
16
38
  const targetSchema = yamlPath.length === 0
17
39
  ? (definition.schema as Record<string, any>)
18
40
  : navigateSchema(definition.schema as Record<string, any>, yamlPath);
19
41
 
20
- if (!targetSchema?.properties) return [];
42
+ if (!targetSchema?.properties) {
43
+ if (yamlPath.length === 0) {
44
+ return buildItems(ROOT_IMPLICIT_PROPS, existingKeys, new Set<string>());
45
+ }
46
+ return [];
47
+ }
21
48
 
22
49
  const required = new Set<string>(
23
50
  Array.isArray(targetSchema.required) ? targetSchema.required : [],
24
51
  );
25
- const items: CompletionResult[] = [];
52
+ const properties =
53
+ yamlPath.length === 0
54
+ ? { ...ROOT_IMPLICIT_PROPS, ...(targetSchema.properties as Record<string, any>) }
55
+ : (targetSchema.properties as Record<string, any>);
26
56
 
27
- for (const [prop, propSchema] of Object.entries(
28
- targetSchema.properties as Record<string, any>,
29
- )) {
57
+ return buildItems(properties, existingKeys, required);
58
+ }
59
+
60
+ function buildItems(
61
+ properties: Record<string, any>,
62
+ existingKeys: Set<string>,
63
+ required: Set<string>,
64
+ ): CompletionResult[] {
65
+ const items: CompletionResult[] = [];
66
+ for (const [prop, propSchema] of Object.entries(properties)) {
30
67
  if (existingKeys.has(prop)) continue;
31
- if (yamlPath.length === 0 && (prop === "kind" || prop === "metadata")) continue;
32
68
 
33
69
  const item: CompletionResult = {
34
70
  label: prop,
@@ -9,16 +9,28 @@ const ZERO_RANGE: Range = {
9
9
  /** Falls back through the chain from the VS Code extension's inline resolver
10
10
  * (ide/vscode/src/extension.ts:203-216 before this package existed):
11
11
  * 1. `d.range` if present.
12
- * 2. `positionIndex.get(d.data.path)` when both are available.
13
- * 3. Whole-line span at `sourceLine` when known.
14
- * 4. `(0,0)-(0,0)` as a last resort. Never undefined. */
12
+ * 2. `positionIndex.get(d.data.path)` for a direct hit (covers diagnostics
13
+ * that target an existing value, e.g. wrong type, enum violation).
14
+ * 3. If the leaf is missing (e.g. "missing required property" — `.type`
15
+ * isn't in the YAML yet), walk one segment up at a time and squiggle
16
+ * just the parent's key identifier (`@key:<parent>`), not the parent's
17
+ * full value block. Keeps the squiggle scoped to the incomplete entry
18
+ * instead of spreading across every line of the surrounding map.
19
+ * 4. Whole-line span at `sourceLine` when known.
20
+ * 5. `(0,0)-(0,0)` as a last resort. Never undefined. */
15
21
  export function resolveRange(d: AnalysisDiagnostic, ctx: DiagnosticContext): Range {
16
22
  if (d.range) return d.range;
17
23
 
18
24
  const fieldPath = (d.data as { path?: string } | undefined)?.path;
19
25
  if (fieldPath !== undefined && ctx.positionIndex) {
20
- const fieldRange = ctx.positionIndex.get(fieldPath);
21
- if (fieldRange) return fieldRange;
26
+ const direct = ctx.positionIndex.get(fieldPath);
27
+ if (direct) return direct;
28
+ for (const parent of parentPaths(fieldPath).slice(1)) {
29
+ const keyRange = ctx.positionIndex.get(`@key:${parent}`);
30
+ if (keyRange) return keyRange;
31
+ const valueRange = ctx.positionIndex.get(parent);
32
+ if (valueRange) return valueRange;
33
+ }
22
34
  }
23
35
 
24
36
  if (ctx.sourceLine !== undefined) {
@@ -30,3 +42,22 @@ export function resolveRange(d: AnalysisDiagnostic, ctx: DiagnosticContext): Ran
30
42
 
31
43
  return ZERO_RANGE;
32
44
  }
45
+
46
+ /** Yield `path`, then progressively shorter parents formed by stripping
47
+ * trailing dotted segments and array index suffixes. For
48
+ * `"secrets.openaiApiKey.type"` → `["secrets.openaiApiKey.type",
49
+ * "secrets.openaiApiKey", "secrets"]`. For `"routes[0].handler"` →
50
+ * `["routes[0].handler", "routes[0]", "routes"]`. */
51
+ function parentPaths(path: string): string[] {
52
+ const out: string[] = [];
53
+ let cur = path;
54
+ while (cur.length > 0) {
55
+ out.push(cur);
56
+ const lastDot = cur.lastIndexOf(".");
57
+ const lastBracket = cur.lastIndexOf("[");
58
+ const cut = Math.max(lastDot, lastBracket);
59
+ if (cut <= 0) break;
60
+ cur = cur.slice(0, cut);
61
+ }
62
+ return out;
63
+ }