@telorun/ide-support 0.6.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 +5 -5
  8. package/dist/completions/import-source.d.ts.map +1 -1
  9. package/dist/completions/import-source.js +15 -15
  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 +45 -6
  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 +16 -16
  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 +57 -6
@@ -0,0 +1,125 @@
1
+ import { parseToAst, } from "@telorun/analyzer";
2
+ import { navigateSchema } from "../completions/detect-context.js";
3
+ import { resolveNodeAtPosition, scalarString, } from "../completions/resolve-node.js";
4
+ import { CAPABILITY_DOCS } from "../completions/valid-capabilities.js";
5
+ /** Docs for the structural keys shared by every module doc, so hover is useful
6
+ * even at the root, where there is no user-authored schema to navigate. */
7
+ const STRUCTURAL_KEY_DOCS = {
8
+ kind: "The resource kind — `Alias.Name` for an imported kind, or a `Telo.*` root kind.",
9
+ metadata: "Resource identity: `name` (kebab-case, dot-free) and optional `namespace`.",
10
+ imports: "Dependency map: PascalCase alias → `namespace/name@version` source string or object.",
11
+ targets: "Boot sequence run after init — references to `Runnable`/`Service` resources or inline invoke steps.",
12
+ variables: "Typed inputs bound from host env vars (`env:` + JSON-Schema `type:`).",
13
+ secrets: "Secret inputs bound from host env vars (`env:` + `type:`).",
14
+ ports: "Inbound ports the app listens on, each bound to a host env var (Application only).",
15
+ exports: "What importers may reference: `kinds` (kind gate) and `resources` (instance singletons).",
16
+ include: "Partial files loaded into this module scope (paths / globs).",
17
+ capability: "The lifecycle role of the kind this definition registers.",
18
+ schema: "JSON Schema for the kind's config fields, with `x-telo-*` annotations.",
19
+ extends: "Alias-form kind this definition specializes (abstract contract or concrete parent).",
20
+ base: "Construction mapping (`super(...)`) over `self` for a concrete-`extends` definition.",
21
+ controllers: "Controller locator (`pkg:npm`) implementing this kind.",
22
+ };
23
+ /** The kind value of the map that directly encloses `keyName` in the value slot. */
24
+ function typeName(t) {
25
+ if (typeof t === "string")
26
+ return t;
27
+ if (t && typeof t === "object" && typeof t.title === "string")
28
+ return t.title;
29
+ return undefined;
30
+ }
31
+ function kindHover(kind, def) {
32
+ if (!def)
33
+ return `\`${kind}\``;
34
+ const lines = [`### ${kind}`];
35
+ const role = def.capability ? `\`${def.capability}\`` : "resource";
36
+ const module = def.metadata?.module ? ` · module \`${def.metadata.module}\`` : "";
37
+ lines.push(`${role}${module}`);
38
+ const schema = def.schema;
39
+ const desc = schema?.description ?? schema?.title;
40
+ if (typeof desc === "string" && desc)
41
+ lines.push("", desc);
42
+ if (def.extends)
43
+ lines.push("", `Extends \`${def.extends}\``);
44
+ const input = typeName(def.inputType);
45
+ const output = typeName(def.outputType);
46
+ if (input)
47
+ lines.push(`Input \`${input}\``);
48
+ if (output)
49
+ lines.push(`Output \`${output}\``);
50
+ return lines.join("\n");
51
+ }
52
+ function fieldHover(keyName, field) {
53
+ const lines = [];
54
+ const type = Array.isArray(field.type) ? field.type.join(" | ") : field.type;
55
+ const head = type ? `**${keyName}**: \`${type}\`` : `**${keyName}**`;
56
+ lines.push(head);
57
+ if (typeof field.description === "string" && field.description) {
58
+ lines.push("", field.description);
59
+ }
60
+ const ref = field["x-telo-ref"];
61
+ if (typeof ref === "string")
62
+ lines.push("", `Reference → \`${ref}\``);
63
+ if (Array.isArray(field.enum) && field.enum.length > 0) {
64
+ lines.push("", `Allowed: ${field.enum.map((v) => `\`${v}\``).join(", ")}`);
65
+ }
66
+ if (field.default !== undefined)
67
+ lines.push(`Default: \`${JSON.stringify(field.default)}\``);
68
+ return lines.length > 0 ? lines.join("\n") : `**${keyName}**`;
69
+ }
70
+ /** Field schema at the nearest enclosing resource, or undefined when the scope
71
+ * can't be resolved (no kind-bearing ancestor, or the path doesn't navigate). */
72
+ function fieldSchemaFor(resourceKind, relativePath, registry) {
73
+ if (!resourceKind || !registry)
74
+ return undefined;
75
+ const def = registry.resolveDefinition(resourceKind);
76
+ if (!def?.schema)
77
+ return undefined;
78
+ return navigateSchema(def.schema, relativePath);
79
+ }
80
+ export function buildHover(text, line, character, registry, docs) {
81
+ const astDocs = docs ?? parseToAst(text);
82
+ const resolved = resolveNodeAtPosition(text, astDocs, line, character);
83
+ if (!resolved)
84
+ return undefined;
85
+ if (resolved.slot === "value")
86
+ return hoverForValue(resolved, registry);
87
+ return hoverForKey(resolved, registry);
88
+ }
89
+ function hoverForValue(resolved, registry) {
90
+ const key = resolved.path[resolved.path.length - 1];
91
+ const value = scalarString(resolved.node);
92
+ const range = resolved.replaceRange;
93
+ if (key === "kind" && value) {
94
+ return { contents: kindHover(value, registry?.resolveDefinition(value)), range };
95
+ }
96
+ if (key === "capability" && resolved.docKind === "Telo.Definition" && value) {
97
+ const doc = CAPABILITY_DOCS[value];
98
+ return doc ? { contents: `**${value}**\n\n${doc}`, range } : undefined;
99
+ }
100
+ // Field value: describe the field via the enclosing resource's schema. Works
101
+ // when the value sits directly under a kind-bearing map (`siblingKind`); the
102
+ // field path relative to that map is just the key.
103
+ if (key) {
104
+ const field = fieldSchemaFor(resolved.siblingKind, [key], registry);
105
+ if (field)
106
+ return { contents: fieldHover(key, field), range };
107
+ }
108
+ return undefined;
109
+ }
110
+ function hoverForKey(resolved, registry) {
111
+ const keyName = scalarString(resolved.node);
112
+ if (!keyName)
113
+ return undefined;
114
+ const range = resolved.replaceRange;
115
+ const resourceKind = resolved.resourceKind ?? resolved.docKind;
116
+ const relativePath = [...resolved.path.slice(resolved.resourceDepth ?? 0), keyName];
117
+ const field = fieldSchemaFor(resourceKind, relativePath, registry);
118
+ if (field)
119
+ return { contents: fieldHover(keyName, field), range };
120
+ const structural = STRUCTURAL_KEY_DOCS[keyName];
121
+ if (structural && relativePath.length === 1) {
122
+ return { contents: `**${keyName}**\n\n${structural}`, range };
123
+ }
124
+ return undefined;
125
+ }
@@ -0,0 +1,2 @@
1
+ export { buildHover } from "./build-hover.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hover/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1 @@
1
+ export { buildHover } from "./build-hover.js";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from "./types.js";
2
2
  export * from "./completions/index.js";
3
3
  export * from "./diagnostics/index.js";
4
+ export * from "./hover/index.js";
5
+ export * from "./semantic-tokens/index.js";
6
+ export * from "./definition/index.js";
4
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
1
  export * from "./types.js";
2
2
  export * from "./completions/index.js";
3
3
  export * from "./diagnostics/index.js";
4
+ export * from "./hover/index.js";
5
+ export * from "./semantic-tokens/index.js";
6
+ export * from "./definition/index.js";
@@ -0,0 +1,12 @@
1
+ import { type AnalysisRegistry, type AstDocument } from "@telorun/analyzer";
2
+ import type { SemanticToken } from "../types.js";
3
+ /** Registry-aware semantic tokens: a `kind:` value that resolves to a known
4
+ * definition is a `type`; a `capability:` value is an `interface`; a `!ref`
5
+ * target is a `variable`. Everything else (structure, CEL, tags) is left to the
6
+ * TextMate grammar. Ref targets are colored here rather than in the grammar
7
+ * because a `!ref` after a `key:` is tokenized by the bundled YAML grammar
8
+ * before a Telo pattern can claim it — the AST sees it unambiguously. An
9
+ * unresolved kind gets no token, so a typo stays uncolored — a quiet signal
10
+ * that pairs with the analyzer's `UNDEFINED_KIND` diagnostic. */
11
+ export declare function buildSemanticTokens(text: string, registry: AnalysisRegistry | undefined, docs?: AstDocument[]): SemanticToken[];
12
+ //# sourceMappingURL=build-semantic-tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-semantic-tokens.d.ts","sourceRoot":"","sources":["../../src/semantic-tokens/build-semantic-tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAEjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD;;;;;;;kEAOkE;AAClE,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,aAAa,EAAE,CAqCjB"}
@@ -0,0 +1,55 @@
1
+ import { buildLineOffsets, offsetToPosition, parseToAst, } from "@telorun/analyzer";
2
+ import { scalarString } from "../completions/resolve-node.js";
3
+ import { CAPABILITY_VALUES } from "../completions/valid-capabilities.js";
4
+ const CAPABILITIES = new Set(CAPABILITY_VALUES);
5
+ /** Registry-aware semantic tokens: a `kind:` value that resolves to a known
6
+ * definition is a `type`; a `capability:` value is an `interface`; a `!ref`
7
+ * target is a `variable`. Everything else (structure, CEL, tags) is left to the
8
+ * TextMate grammar. Ref targets are colored here rather than in the grammar
9
+ * because a `!ref` after a `key:` is tokenized by the bundled YAML grammar
10
+ * before a Telo pattern can claim it — the AST sees it unambiguously. An
11
+ * unresolved kind gets no token, so a typo stays uncolored — a quiet signal
12
+ * that pairs with the analyzer's `UNDEFINED_KIND` diagnostic. */
13
+ export function buildSemanticTokens(text, registry, docs) {
14
+ const astDocs = docs ?? parseToAst(text);
15
+ const lineOffsets = buildLineOffsets(text);
16
+ const tokens = [];
17
+ const emit = (node, type) => {
18
+ if (!node)
19
+ return;
20
+ const start = offsetToPosition(node.range[0], lineOffsets);
21
+ const end = offsetToPosition(node.range[1], lineOffsets);
22
+ // Kind / capability values never span lines; a clamped single-line token.
23
+ if (start.line !== end.line)
24
+ return;
25
+ tokens.push({ line: start.line, character: start.character, length: end.character - start.character, type });
26
+ };
27
+ const walk = (node) => {
28
+ if (node.kind === "map") {
29
+ for (const pair of node.entries) {
30
+ const key = scalarString(pair.key);
31
+ const value = scalarString(pair.value);
32
+ if (key === "kind" && value && registry?.resolveDefinition(value)) {
33
+ emit(pair.value, "type");
34
+ }
35
+ else if (key === "capability" && value && CAPABILITIES.has(value)) {
36
+ emit(pair.value, "interface");
37
+ }
38
+ if (pair.value)
39
+ walk(pair.value);
40
+ }
41
+ }
42
+ else if (node.kind === "seq") {
43
+ for (const item of node.items)
44
+ walk(item);
45
+ }
46
+ else if (node.kind === "scalar" && node.tag === "!ref") {
47
+ emit(node, "variable");
48
+ }
49
+ };
50
+ for (const doc of astDocs) {
51
+ if (doc.root)
52
+ walk(doc.root);
53
+ }
54
+ return tokens;
55
+ }
@@ -0,0 +1,2 @@
1
+ export { buildSemanticTokens } from "./build-semantic-tokens.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/semantic-tokens/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1 @@
1
+ export { buildSemanticTokens } from "./build-semantic-tokens.js";
package/dist/types.d.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  export { AnalysisRegistry, DiagnosticSeverity } from "@telorun/analyzer";
2
2
  export type { Position, Range, AnalysisDiagnostic, PositionIndex, } from "@telorun/analyzer";
3
- import type { AnalysisRegistry, DiagnosticSeverity, PositionIndex, Range } from "@telorun/analyzer";
3
+ import type { AnalysisRegistry, DiagnosticSeverity, Position, PositionIndex, Range } from "@telorun/analyzer";
4
4
  export type CompletionKind = "class" | "enumMember" | "property" | "folder" | "module" | "value";
5
+ /** A source span the host replaces wholesale when a completion is accepted. */
6
+ export interface ReplaceRange {
7
+ start: Position;
8
+ end: Position;
9
+ }
5
10
  export interface CompletionResult {
6
11
  label: string;
7
12
  kind: CompletionKind;
@@ -12,11 +17,45 @@ export interface CompletionResult {
12
17
  preselect?: boolean;
13
18
  sortText?: string;
14
19
  filterText?: string;
15
- /** When set, the host should replace text from this 0-based column on the
16
- * cursor's line up to the cursor. Required when the completion value
17
- * contains non-word characters (`/`, `@`, `.`) that the host's default
18
- * word boundary would not include in the replaced range. */
19
- replaceFromColumn?: number;
20
+ /** When set, the host replaces this whole source range with the accepted
21
+ * value the full span of the existing node, not just the prefix up to the
22
+ * cursor. This overwrites any suffix after the cursor (`Sql.Co|nnection` +
23
+ * `Sql.Connection` no leftover `nnection`) and cleanly replaces values
24
+ * containing non-word characters (`/`, `@`, `.`). A zero-width range is a
25
+ * pure insert. */
26
+ replaceRange?: ReplaceRange;
27
+ }
28
+ /** Rendered hover for the symbol under the cursor. `contents` is GitHub-flavored
29
+ * markdown; `range` (when present) is the source span the host underlines. */
30
+ export interface HoverResult {
31
+ contents: string;
32
+ range?: ReplaceRange;
33
+ }
34
+ /** Semantic token type names emitted by `buildSemanticTokens`. Kept to the
35
+ * standard VS Code / LSP set so hosts register them against a stock legend and
36
+ * every theme colors them without extra configuration. `type` marks a resolved
37
+ * resource kind; `interface` marks a capability value; `variable` marks a
38
+ * `!ref` target. */
39
+ export type SemanticTokenType = "type" | "interface" | "variable";
40
+ /** The legend a host registers before mapping `buildSemanticTokens` output. The
41
+ * numeric token-type of each `SemanticToken` is its index in this array. */
42
+ export declare const SEMANTIC_TOKEN_LEGEND: readonly SemanticTokenType[];
43
+ /** One absolute-positioned semantic token. Every Telo semantic token is
44
+ * single-line (kinds and capabilities never wrap), so a `{line, char, length}`
45
+ * triple is sufficient; the host encodes it into its own builder. */
46
+ export interface SemanticToken {
47
+ line: number;
48
+ character: number;
49
+ length: number;
50
+ type: SemanticTokenType;
51
+ }
52
+ /** Where a `!ref` target is defined — for go-to-definition. `uri` is the
53
+ * target file's canonical source (absolute path for local files, an http/oci
54
+ * URL for a registry import); `range` spans the target resource's
55
+ * `metadata.name` (falling back to its first line). */
56
+ export interface DefinitionResult {
57
+ uri: string;
58
+ range: Range;
20
59
  }
21
60
  /** A candidate module ref surfaced by the hub's `/refs` lexical autocomplete.
22
61
  * Identity is the location ref, never `namespace/name` — an OCI module has no
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGzE,YAAY,EACV,QAAQ,EACR,KAAK,EACL,kBAAkB,EAClB,aAAa,GACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEpG,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjG,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;iEAG6D;IAC7D,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;wDAGwD;AACxD,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;uCAMuC;AACvC,MAAM,WAAW,qBAAqB;IACpC;;2DAEuD;IACvD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpD;kEAC8D;IAC9D,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C;;;oFAGgF;IAChF,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7C;;0CAEsC;IACtC,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnE;;;0EAGsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -1,3 +1,6 @@
1
1
  // Runtime values (classes, enums) — consumers who need `new AnalysisRegistry()`
2
2
  // or `DiagnosticSeverity.Error` import from here.
3
3
  export { AnalysisRegistry, DiagnosticSeverity } from "@telorun/analyzer";
4
+ /** The legend a host registers before mapping `buildSemanticTokens` output. The
5
+ * numeric token-type of each `SemanticToken` is its index in this array. */
6
+ export const SEMANTIC_TOKEN_LEGEND = ["type", "interface", "variable"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/ide-support",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Editor-host-agnostic IDE support (completions, diagnostic normalization) for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -36,7 +36,7 @@
36
36
  "src/**"
37
37
  ],
38
38
  "dependencies": {
39
- "@telorun/analyzer": "0.40.0"
39
+ "@telorun/analyzer": "0.41.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^20.0.0",
@@ -1,5 +1,6 @@
1
- import type { AnalysisRegistry } from "@telorun/analyzer";
1
+ import { parseToAst, type AnalysisRegistry, type AstDocument, type AstMap } from "@telorun/analyzer";
2
2
  import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
3
+ import type { ReplaceRange } from "./detect-context.js";
3
4
  import { detectContext, lookupRefConstraint } from "./detect-context.js";
4
5
  import { importSourceCompletions } from "./import-source.js";
5
6
  import { propKeyCompletions } from "./prop-keys.js";
@@ -10,56 +11,29 @@ interface ResourceRecord {
10
11
  name: string;
11
12
  }
12
13
 
13
- /** Roughly extract `(kind, metadata.name)` pairs from a multi-doc YAML text.
14
- * This is intentionally lightweight: it scans for top-level `kind:` and the
15
- * first `name:` under a `metadata:` block per `---`-separated section, with
16
- * no full YAML parse. The output is consumed only for completion ranking,
17
- * so misses on edge-case manifests are acceptable; the analyzer remains
18
- * the source of truth for validation. */
19
- function extractInFileResources(text: string): ResourceRecord[] {
14
+ /** Read the top-level `kind` and `metadata.name` scalar of each document from
15
+ * the AST. Consumed only for ref-name completion ranking, so a doc missing
16
+ * either is simply skipped; the analyzer remains the source of truth. */
17
+ function extractInFileResources(docs: AstDocument[]): ResourceRecord[] {
20
18
  const out: ResourceRecord[] = [];
21
- const lines = text.split("\n");
22
- let currentKind: string | undefined;
23
- let currentName: string | undefined;
24
- let inMetadata = false;
19
+ const scalar = (node: { kind: string; value?: unknown } | undefined): string | undefined =>
20
+ node?.kind === "scalar" && typeof node.value === "string" ? node.value : undefined;
25
21
 
26
- const flush = () => {
27
- if (currentKind && currentName) {
28
- out.push({ kind: currentKind, name: currentName });
29
- }
30
- currentKind = undefined;
31
- currentName = undefined;
32
- inMetadata = false;
33
- };
34
-
35
- for (const line of lines) {
36
- if (line.trimEnd() === "---") {
37
- flush();
38
- continue;
39
- }
40
- const kindMatch = line.match(/^kind:\s*(\S+)/);
41
- if (kindMatch) {
42
- currentKind = kindMatch[1];
43
- continue;
44
- }
45
- if (/^metadata:\s*$/.test(line)) {
46
- inMetadata = true;
47
- continue;
48
- }
49
- if (inMetadata) {
50
- // Lines inside metadata are indented. Pick the first `name:` we see.
51
- const nameMatch = line.match(/^\s+name:\s*(\S+)/);
52
- if (nameMatch && !currentName) {
53
- currentName = nameMatch[1];
54
- }
55
- // Leaving the metadata block — any line that is not indented marks
56
- // the end of the block.
57
- if (line.length > 0 && !/^\s/.test(line)) {
58
- inMetadata = false;
22
+ for (const doc of docs) {
23
+ if (doc.root?.kind !== "map") continue;
24
+ let kind: string | undefined;
25
+ let name: string | undefined;
26
+ for (const pair of doc.root.entries) {
27
+ const key = scalar(pair.key);
28
+ if (key === "kind") kind = scalar(pair.value);
29
+ else if (key === "metadata" && pair.value?.kind === "map") {
30
+ const meta = pair.value as AstMap;
31
+ const nameEntry = meta.entries.find((e) => scalar(e.key) === "name");
32
+ name = scalar(nameEntry?.value);
59
33
  }
60
34
  }
35
+ if (kind && name) out.push({ kind, name });
61
36
  }
62
- flush();
63
37
  return out;
64
38
  }
65
39
 
@@ -71,13 +45,13 @@ function extractInFileResources(text: string): ResourceRecord[] {
71
45
  * user still sees something rather than nothing when the registry
72
46
  * doesn't recognize the kind yet. */
73
47
  function refNameCompletions(
74
- text: string,
48
+ docs: AstDocument[],
75
49
  refKind: string | undefined,
76
50
  refConstraint: string | undefined,
77
51
  registry: AnalysisRegistry | undefined,
78
- valueStartColumn: number,
52
+ replaceRange: ReplaceRange,
79
53
  ): CompletionResult[] {
80
- const resources = extractInFileResources(text);
54
+ const resources = extractInFileResources(docs);
81
55
  let acceptable: Set<string> | undefined;
82
56
 
83
57
  if (refKind) {
@@ -97,10 +71,9 @@ function refNameCompletions(
97
71
  label: r.name,
98
72
  kind: "value",
99
73
  detail: r.kind,
100
- // Anchor the replace range to the value's start column so names with
101
- // `.`, `-`, or `/` (legal in resource names) replace the whole typed
102
- // prefix instead of the trailing word VS Code would pick by default.
103
- replaceFromColumn: valueStartColumn,
74
+ // Replace the whole existing value so names with `.`, `-`, or `/` (legal
75
+ // in resource names) overwrite cleanly instead of the trailing word.
76
+ replaceRange,
104
77
  });
105
78
  }
106
79
  return out;
@@ -129,7 +102,7 @@ function kindCompletions(
129
102
  registry: AnalysisRegistry | undefined,
130
103
  docKind: string | undefined,
131
104
  yamlPath: string[] | undefined,
132
- valueStartColumn: number | undefined,
105
+ replaceRange: ReplaceRange,
133
106
  ): CompletionResult[] {
134
107
  let kinds: Iterable<string>;
135
108
  if (registry && docKind && yamlPath && yamlPath.length > 0) {
@@ -145,14 +118,10 @@ function kindCompletions(
145
118
  for (const kind of kinds) {
146
119
  if (seen.has(kind)) continue;
147
120
  seen.add(kind);
148
- const item: CompletionResult = { label: kind, kind: "class", detail: "Telo resource kind" };
149
- // Anchor the replace range to the value's start column so kinds with `.`
150
- // (e.g. `Sql.Connection`) cleanly overwrite the existing prefix. Without
151
- // this, VS Code's default word boundary stops at the last `.` and a pick
152
- // of `Sql.Connection` while the buffer reads `Sql.Co|` becomes
153
- // `Sql.Sql.Connection`.
154
- if (valueStartColumn !== undefined) item.replaceFromColumn = valueStartColumn;
155
- results.push(item);
121
+ // Replace the whole existing kind scalar so a pick of `Sql.Connection`
122
+ // over `Sql.Co|nnection` leaves no `nnection` suffix and no `Sql.` prefix
123
+ // duplication (VS Code's default word range stops at the last `.`).
124
+ results.push({ label: kind, kind: "class", detail: "Telo resource kind", replaceRange });
156
125
  }
157
126
  return results;
158
127
  }
@@ -171,11 +140,16 @@ export async function buildCompletions(
171
140
  character: number,
172
141
  registry: AnalysisRegistry | undefined,
173
142
  adapter?: IdeEnvironmentAdapter,
143
+ docs?: AstDocument[],
174
144
  ): Promise<CompletionResult[]> {
175
- const ctx = detectContext(text, line, character);
145
+ // Reuse the host's already-parsed AST when it matches the current buffer;
146
+ // otherwise parse once here (Part 1 stands alone). Both `detectContext` and
147
+ // ref-name in-file resource extraction share this single parse.
148
+ const astDocs = docs ?? parseToAst(text);
149
+ const ctx = detectContext(text, line, character, astDocs);
176
150
  if (!ctx) return [];
177
151
  if (ctx.type === "kind") {
178
- return kindCompletions(registry, ctx.docKind, ctx.yamlPath, ctx.valueStartColumn);
152
+ return kindCompletions(registry, ctx.docKind, ctx.yamlPath, ctx.replaceRange);
179
153
  }
180
154
  if (ctx.type === "capability") return capabilityCompletions();
181
155
  if (ctx.type === "ref-name") {
@@ -183,17 +157,11 @@ export async function buildCompletions(
183
157
  const refConstraint = definition?.schema
184
158
  ? lookupRefConstraint(definition.schema as Record<string, any>, ctx.yamlPath)
185
159
  : undefined;
186
- return refNameCompletions(
187
- text,
188
- ctx.refKind,
189
- refConstraint,
190
- registry,
191
- ctx.valueStartColumn,
192
- );
160
+ return refNameCompletions(astDocs, ctx.refKind, refConstraint, registry, ctx.replaceRange);
193
161
  }
194
162
  if (ctx.type === "field-value") {
195
163
  if (ctx.field === "import-source") {
196
- return importSourceCompletions(ctx.prefix, ctx.valueStartColumn, adapter);
164
+ return importSourceCompletions(ctx.prefix, ctx.replaceRange, adapter);
197
165
  }
198
166
  return [];
199
167
  }