@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.
- package/dist/completions/build.d.ts +2 -2
- package/dist/completions/build.d.ts.map +1 -1
- package/dist/completions/build.js +39 -64
- package/dist/completions/detect-context.d.ts +16 -43
- package/dist/completions/detect-context.d.ts.map +1 -1
- package/dist/completions/detect-context.js +63 -262
- package/dist/completions/import-source.d.ts +5 -5
- package/dist/completions/import-source.d.ts.map +1 -1
- package/dist/completions/import-source.js +15 -15
- package/dist/completions/resolve-node.d.ts +50 -0
- package/dist/completions/resolve-node.d.ts.map +1 -0
- package/dist/completions/resolve-node.js +269 -0
- package/dist/completions/valid-capabilities.d.ts +3 -0
- package/dist/completions/valid-capabilities.d.ts.map +1 -1
- package/dist/completions/valid-capabilities.js +10 -0
- package/dist/definition/build-definition.d.ts +13 -0
- package/dist/definition/build-definition.d.ts.map +1 -0
- package/dist/definition/build-definition.js +98 -0
- package/dist/definition/index.d.ts +2 -0
- package/dist/definition/index.d.ts.map +1 -0
- package/dist/definition/index.js +1 -0
- package/dist/hover/build-hover.d.ts +4 -0
- package/dist/hover/build-hover.d.ts.map +1 -0
- package/dist/hover/build-hover.js +125 -0
- package/dist/hover/index.d.ts +2 -0
- package/dist/hover/index.d.ts.map +1 -0
- package/dist/hover/index.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/semantic-tokens/build-semantic-tokens.d.ts +12 -0
- package/dist/semantic-tokens/build-semantic-tokens.d.ts.map +1 -0
- package/dist/semantic-tokens/build-semantic-tokens.js +55 -0
- package/dist/semantic-tokens/index.d.ts +2 -0
- package/dist/semantic-tokens/index.d.ts.map +1 -0
- package/dist/semantic-tokens/index.js +1 -0
- package/dist/types.d.ts +45 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/package.json +2 -2
- package/src/completions/build.ts +40 -72
- package/src/completions/detect-context.ts +74 -291
- package/src/completions/import-source.ts +16 -16
- package/src/completions/resolve-node.ts +405 -0
- package/src/completions/valid-capabilities.ts +11 -0
- package/src/definition/build-definition.ts +123 -0
- package/src/definition/index.ts +1 -0
- package/src/hover/build-hover.ts +149 -0
- package/src/hover/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/semantic-tokens/build-semantic-tokens.ts +64 -0
- package/src/semantic-tokens/index.ts +1 -0
- 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 @@
|
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
@@ -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 @@
|
|
|
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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.
|
|
39
|
+
"@telorun/analyzer": "0.41.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^20.0.0",
|
package/src/completions/build.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
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
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
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
|
|
22
|
-
|
|
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
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
48
|
+
docs: AstDocument[],
|
|
75
49
|
refKind: string | undefined,
|
|
76
50
|
refConstraint: string | undefined,
|
|
77
51
|
registry: AnalysisRegistry | undefined,
|
|
78
|
-
|
|
52
|
+
replaceRange: ReplaceRange,
|
|
79
53
|
): CompletionResult[] {
|
|
80
|
-
const resources = extractInFileResources(
|
|
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
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
//
|
|
150
|
-
// (
|
|
151
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
164
|
+
return importSourceCompletions(ctx.prefix, ctx.replaceRange, adapter);
|
|
197
165
|
}
|
|
198
166
|
return [];
|
|
199
167
|
}
|