@telorun/ide-support 0.8.0 → 0.9.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 (36) hide show
  1. package/README.md +3 -1
  2. package/dist/completions/import-source.js +2 -2
  3. package/dist/completions/resolve-node.d.ts +5 -0
  4. package/dist/completions/resolve-node.d.ts.map +1 -1
  5. package/dist/completions/resolve-node.js +4 -0
  6. package/dist/definition/alias-qualified-value.d.ts +23 -0
  7. package/dist/definition/alias-qualified-value.d.ts.map +1 -0
  8. package/dist/definition/alias-qualified-value.js +24 -0
  9. package/dist/definition/build-definition.d.ts +15 -8
  10. package/dist/definition/build-definition.d.ts.map +1 -1
  11. package/dist/definition/build-definition.js +35 -89
  12. package/dist/definition/manifest-navigation.d.ts +36 -0
  13. package/dist/definition/manifest-navigation.d.ts.map +1 -0
  14. package/dist/definition/manifest-navigation.js +92 -0
  15. package/dist/definition/resolve-cel-target.d.ts +17 -0
  16. package/dist/definition/resolve-cel-target.d.ts.map +1 -0
  17. package/dist/definition/resolve-cel-target.js +125 -0
  18. package/dist/definition/resolve-export-chain.d.ts +23 -0
  19. package/dist/definition/resolve-export-chain.d.ts.map +1 -0
  20. package/dist/definition/resolve-export-chain.js +89 -0
  21. package/dist/definition/resolve-kind-target.d.ts +26 -0
  22. package/dist/definition/resolve-kind-target.d.ts.map +1 -0
  23. package/dist/definition/resolve-kind-target.js +51 -0
  24. package/dist/definition/resolve-ref-target.d.ts +14 -0
  25. package/dist/definition/resolve-ref-target.d.ts.map +1 -0
  26. package/dist/definition/resolve-ref-target.js +25 -0
  27. package/package.json +2 -2
  28. package/src/completions/import-source.ts +2 -2
  29. package/src/completions/resolve-node.ts +9 -0
  30. package/src/definition/alias-qualified-value.ts +42 -0
  31. package/src/definition/build-definition.ts +34 -104
  32. package/src/definition/manifest-navigation.ts +124 -0
  33. package/src/definition/resolve-cel-target.ts +151 -0
  34. package/src/definition/resolve-export-chain.ts +119 -0
  35. package/src/definition/resolve-kind-target.ts +61 -0
  36. package/src/definition/resolve-ref-target.ts +34 -0
@@ -0,0 +1,89 @@
1
+ import { parseExportEntry, resolveExportedKinds, } from "@telorun/analyzer";
2
+ import { locateKindDefinition, locateResource, moduleDoc, moduleFiles, moduleName, } from "./manifest-navigation.js";
3
+ /** One `exports.*` list as authored. `undefined` means the block is ABSENT,
4
+ * which is not the same as an empty list — the distinction is the gate itself,
5
+ * and it is load-bearing in both halves (`AliasResolver.registerImport`,
6
+ * `ModuleContext.buildExportTable`), so it must survive here too. */
7
+ function exportList(mod, block) {
8
+ const exports = moduleDoc(mod)?.exports;
9
+ const list = exports?.[block];
10
+ if (!Array.isArray(list))
11
+ return undefined;
12
+ return list.filter((e) => typeof e === "string");
13
+ }
14
+ /** Every loaded module keyed by its `metadata.name` — what `resolveExportedKinds`
15
+ * speaks, since a canonical kind names its owning module rather than a URL. */
16
+ function modulesByName(graph) {
17
+ const byName = new Map();
18
+ for (const mod of graph.modules.values()) {
19
+ const name = moduleName(mod);
20
+ if (name && !byName.has(name))
21
+ byName.set(name, mod);
22
+ }
23
+ return byName;
24
+ }
25
+ /** Resolve the kind suffix `name` exported by the module at `targetSource` to
26
+ * the `Telo.Definition` / `Telo.Abstract` doc that owns it.
27
+ *
28
+ * The gate and the transitive re-export chain come from the analyzer's own
29
+ * `resolveExportedKinds` fixpoint rather than a local walk, so navigation
30
+ * cannot disagree with `telo check` about what an import exposes — including
31
+ * the two rules a hand-rolled walk gets wrong: `exports.kinds: []` gates
32
+ * everything while an absent block gates nothing, and a re-export FROM an
33
+ * ungated module resolves straight to it. */
34
+ export function resolveExportedKind(graph, targetSource, name) {
35
+ const target = graph.modules.get(targetSource);
36
+ if (!target)
37
+ return undefined;
38
+ // No `exports.kinds` block → every kind the module defines is importable (the
39
+ // legacy permissive default the kernel still honors for already-published
40
+ // versions). The fixpoint builds tables only from declared entries, so an
41
+ // ungated module has none to look this up in — and this is also the cheap
42
+ // path, so it settles before anything is indexed.
43
+ if (exportList(target, "kinds") === undefined) {
44
+ return locateKindDefinition(moduleFiles(target), name);
45
+ }
46
+ const targetName = moduleName(target);
47
+ if (!targetName)
48
+ return undefined;
49
+ const byName = modulesByName(graph);
50
+ const tables = resolveExportedKinds([...byName].map(([module, mod]) => ({ module, exportsKinds: exportList(mod, "kinds") })), (module, alias) => {
51
+ const owner = byName.get(module)?.owner.source;
52
+ return owner ? graph.importEdges.get(owner)?.get(alias)?.targetModuleName ?? undefined : undefined;
53
+ });
54
+ const canonical = tables.get(targetName)?.get(name);
55
+ if (!canonical)
56
+ return undefined;
57
+ // `<owningModule>.<Kind>` — a re-export resolves to its true owner, so the
58
+ // jump lands where the kind is actually declared, however many hops away.
59
+ const dot = canonical.lastIndexOf(".");
60
+ const owner = byName.get(canonical.slice(0, dot));
61
+ return owner ? locateKindDefinition(moduleFiles(owner), canonical.slice(dot + 1)) : undefined;
62
+ }
63
+ /** Resolve the instance `name` exported by the module at `targetSource`,
64
+ * following `<Alias>.<name>` re-exports transitively. `seen` bounds cyclic
65
+ * import graphs.
66
+ *
67
+ * Unlike kinds there is no permissive default: the kernel reads
68
+ * `exports.resources ?? []` and builds its export table strictly from that, so
69
+ * an absent block exports nothing and an unlisted name is an
70
+ * `UNRESOLVED_REFERENCE` — navigating to it would tell the author a wiring is
71
+ * real that the analyzer rejects. */
72
+ export function resolveExportedResource(graph, targetSource, name, seen = new Set()) {
73
+ if (seen.has(targetSource))
74
+ return undefined;
75
+ seen.add(targetSource);
76
+ const target = graph.modules.get(targetSource);
77
+ if (!target)
78
+ return undefined;
79
+ const entries = (exportList(target, "resources") ?? []).map(parseExportEntry);
80
+ const entry = entries.find((e) => e.name === name);
81
+ if (!entry)
82
+ return undefined;
83
+ // `Self.<name>` names the declaring module's own instance, exactly as a bare
84
+ // name does — `ModuleContext.buildExportTable` treats the two alike.
85
+ if (!entry.alias || entry.alias === "Self")
86
+ return locateResource(moduleFiles(target), name);
87
+ const edge = graph.importEdges.get(target.owner.source)?.get(entry.alias);
88
+ return edge ? resolveExportedResource(graph, edge.targetSource, name, seen) : undefined;
89
+ }
@@ -0,0 +1,26 @@
1
+ import type { LoadedGraph, LoadedModule } from "@telorun/analyzer";
2
+ import type { DefinitionResult } from "../types.js";
3
+ import type { ResolvedCursor } from "../completions/resolve-node.js";
4
+ import type { AliasQualifiedValue } from "./alias-qualified-value.js";
5
+ /** Whether the cursor sits in a slot whose value is an alias-qualified kind.
6
+ *
7
+ * `x-telo-ref` is an `x-telo-*` annotation, unambiguous wherever it appears.
8
+ * `extends:` is only a kind at the top level of a definition doc, so it is
9
+ * gated on both. `kind:` is positional by nature: a map carrying a
10
+ * `<Alias>.<Kind>` value under `kind` IS an inline resource declaration as far
11
+ * as the analyzer is concerned too (`resourceKindOf`), so there is no further
12
+ * structure to check — an unresolvable value simply navigates nowhere.
13
+ *
14
+ * `capability:` is deliberately absent — its values are kernel built-ins with
15
+ * no manifest to jump to (hover documents them instead). */
16
+ export declare function isKindSlot(resolved: ResolvedCursor): boolean;
17
+ /** Resolve an alias-qualified kind (`Http.Server`) to the `Telo.Definition` /
18
+ * `Telo.Abstract` doc that registers it, or — when the cursor sits on the
19
+ * alias — to the import that declares the alias.
20
+ *
21
+ * `Self.<Kind>` is the declaring module's own kind; `Telo.<Kind>` is a kernel
22
+ * built-in with no manifest, so it resolves to nothing. Across an import the
23
+ * walk honors `exports.kinds` and follows `<Inner>.<Kind>` re-exports to the
24
+ * owning module, matching what the kernel resolves the kind to at runtime. */
25
+ export declare function resolveKindTarget(graph: LoadedGraph, currentModule: LoadedModule, kind: AliasQualifiedValue): DefinitionResult | undefined;
26
+ //# sourceMappingURL=resolve-kind-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-kind-target.d.ts","sourceRoot":"","sources":["../../src/definition/resolve-kind-target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAMtE;;;;;;;;;;6DAU6D;AAC7D,wBAAgB,UAAU,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAQ5D;AAED;;;;;;;+EAO+E;AAC/E,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,YAAY,EAC3B,IAAI,EAAE,mBAAmB,GACxB,gBAAgB,GAAG,SAAS,CAkB9B"}
@@ -0,0 +1,51 @@
1
+ import { locateImport, locateKindDefinition, moduleFiles } from "./manifest-navigation.js";
2
+ import { resolveExportedKind } from "./resolve-export-chain.js";
3
+ const DEFINITION_DOC_KINDS = new Set(["Telo.Definition", "Telo.Abstract"]);
4
+ /** Whether the cursor sits in a slot whose value is an alias-qualified kind.
5
+ *
6
+ * `x-telo-ref` is an `x-telo-*` annotation, unambiguous wherever it appears.
7
+ * `extends:` is only a kind at the top level of a definition doc, so it is
8
+ * gated on both. `kind:` is positional by nature: a map carrying a
9
+ * `<Alias>.<Kind>` value under `kind` IS an inline resource declaration as far
10
+ * as the analyzer is concerned too (`resourceKindOf`), so there is no further
11
+ * structure to check — an unresolvable value simply navigates nowhere.
12
+ *
13
+ * `capability:` is deliberately absent — its values are kernel built-ins with
14
+ * no manifest to jump to (hover documents them instead). */
15
+ export function isKindSlot(resolved) {
16
+ const key = resolved.path[resolved.path.length - 1];
17
+ if (key === "x-telo-ref" || key === "kind")
18
+ return true;
19
+ return (key === "extends" &&
20
+ resolved.path.length === 1 &&
21
+ DEFINITION_DOC_KINDS.has(resolved.docKind ?? ""));
22
+ }
23
+ /** Resolve an alias-qualified kind (`Http.Server`) to the `Telo.Definition` /
24
+ * `Telo.Abstract` doc that registers it, or — when the cursor sits on the
25
+ * alias — to the import that declares the alias.
26
+ *
27
+ * `Self.<Kind>` is the declaring module's own kind; `Telo.<Kind>` is a kernel
28
+ * built-in with no manifest, so it resolves to nothing. Across an import the
29
+ * walk honors `exports.kinds` and follows `<Inner>.<Kind>` re-exports to the
30
+ * owning module, matching what the kernel resolves the kind to at runtime. */
31
+ export function resolveKindTarget(graph, currentModule, kind) {
32
+ const { alias, name, onAlias } = kind;
33
+ if (alias === undefined) {
34
+ // The legacy `<namespace>/<module>#<Kind>` identity form of `x-telo-ref`
35
+ // still resolves for already-published module versions, but it names a
36
+ // module this manifest need not import, so there is no alias to follow.
37
+ if (name.includes("#"))
38
+ return undefined;
39
+ return locateKindDefinition(moduleFiles(currentModule), name);
40
+ }
41
+ if (alias === "Self")
42
+ return locateKindDefinition(moduleFiles(currentModule), name);
43
+ if (alias === "Telo")
44
+ return undefined;
45
+ if (onAlias)
46
+ return locateImport(currentModule, alias);
47
+ const edge = graph.importEdges.get(currentModule.owner.source)?.get(alias);
48
+ if (!edge)
49
+ return undefined;
50
+ return resolveExportedKind(graph, edge.targetSource, name);
51
+ }
@@ -0,0 +1,14 @@
1
+ import type { LoadedGraph, LoadedModule } from "@telorun/analyzer";
2
+ import type { DefinitionResult } from "../types.js";
3
+ import type { AliasQualifiedValue } from "./alias-qualified-value.js";
4
+ /** Resolve a `!ref` target to the resource instance it names.
5
+ *
6
+ * The grammar mirrors `resolveRefSentinels`: a bare name (or `Self.name`) is a
7
+ * local resource in the current module; `Alias.name` is an exported instance of
8
+ * the module the import `Alias` points at, followed transitively through
9
+ * re-exports and gated on each module's `exports.resources`. The alias half
10
+ * navigates to the import that declares it. Returns `undefined` when the target
11
+ * can't be found (a scope-local name, an instance the target does not export,
12
+ * or an import that failed to load). */
13
+ export declare function resolveRefTarget(graph: LoadedGraph, currentModule: LoadedModule, ref: AliasQualifiedValue): DefinitionResult | undefined;
14
+ //# sourceMappingURL=resolve-ref-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-ref-target.d.ts","sourceRoot":"","sources":["../../src/definition/resolve-ref-target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAItE;;;;;;;;yCAQyC;AACzC,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,YAAY,EAC3B,GAAG,EAAE,mBAAmB,GACvB,gBAAgB,GAAG,SAAS,CAc9B"}
@@ -0,0 +1,25 @@
1
+ import { locateImport, locateResource, moduleFiles } from "./manifest-navigation.js";
2
+ import { resolveExportedResource } from "./resolve-export-chain.js";
3
+ /** Resolve a `!ref` target to the resource instance it names.
4
+ *
5
+ * The grammar mirrors `resolveRefSentinels`: a bare name (or `Self.name`) is a
6
+ * local resource in the current module; `Alias.name` is an exported instance of
7
+ * the module the import `Alias` points at, followed transitively through
8
+ * re-exports and gated on each module's `exports.resources`. The alias half
9
+ * navigates to the import that declares it. Returns `undefined` when the target
10
+ * can't be found (a scope-local name, an instance the target does not export,
11
+ * or an import that failed to load). */
12
+ export function resolveRefTarget(graph, currentModule, ref) {
13
+ const { alias, name, onAlias } = ref;
14
+ if (alias === undefined || alias === "Self") {
15
+ // `Self` names the declaring module itself — there is no import to jump to,
16
+ // so the alias half resolves like the name half.
17
+ return locateResource(moduleFiles(currentModule), name);
18
+ }
19
+ if (onAlias)
20
+ return locateImport(currentModule, alias);
21
+ const edge = graph.importEdges.get(currentModule.owner.source)?.get(alias);
22
+ if (!edge)
23
+ return undefined;
24
+ return resolveExportedResource(graph, edge.targetSource, name);
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/ide-support",
3
- "version": "0.8.0",
3
+ "version": "0.9.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.50.0"
39
+ "@telorun/analyzer": "0.51.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^20.0.0",
@@ -161,8 +161,8 @@ async function refSearchCompletions(
161
161
 
162
162
  /** The `org/name` tail of a location ref: its last two path segments, with the
163
163
  * transport scheme (`oci://`, `https://`, …) and registry host dropped.
164
- * `oci://ghcr.io/telorun/telo-console` → `telorun/telo-console`; `std/console`
165
- * → `std/console`. Falls back to fewer segments (or the whole ref) when there
164
+ * `oci://ghcr.io/telorun/telo-console` → `telorun/telo-console`; `acme/console`
165
+ * → `acme/console`. Falls back to fewer segments (or the whole ref) when there
166
166
  * aren't two. */
167
167
  function refDisplayName(ref: string): string {
168
168
  const withoutScheme = ref.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
@@ -16,6 +16,11 @@ import {
16
16
  * "which container am I typing into". */
17
17
  export interface ResolvedCursor {
18
18
  docIndex: number;
19
+ /** The cursor as a document offset. Carried so a consumer hit-testing inside a
20
+ * node (a CEL chain, the halves of an `Alias.Name` value) reuses the one this
21
+ * resolution was performed with, rather than recomputing the line table and
22
+ * risking a different answer. */
23
+ offset: number;
19
24
  /** Top-level `kind:` value of the cursor's document, when present. */
20
25
  docKind?: string;
21
26
  slot: "key" | "value";
@@ -332,6 +337,7 @@ export function resolveNodeAtPosition(
332
337
  }
333
338
  return {
334
339
  docIndex,
340
+ offset,
335
341
  docKind,
336
342
  slot: "key",
337
343
  path: found.path,
@@ -362,6 +368,7 @@ export function resolveNodeAtPosition(
362
368
  const { path, existingKeys, scope } = columnSearch(doc.root, col, offset, lineOffsets);
363
369
  return {
364
370
  docIndex,
371
+ offset,
365
372
  docKind,
366
373
  slot: "key",
367
374
  path,
@@ -375,6 +382,7 @@ export function resolveNodeAtPosition(
375
382
  const clampedEnd = Math.min(offset, value.range[1]);
376
383
  return {
377
384
  docIndex,
385
+ offset,
378
386
  docKind,
379
387
  slot: "value",
380
388
  path: found.keyName != null ? [...found.path, found.keyName] : found.path,
@@ -395,6 +403,7 @@ export function resolveNodeAtPosition(
395
403
  : { path: [], existingKeys: new Set<string>(), scope: { depth: 0 } };
396
404
  return {
397
405
  docIndex,
406
+ offset,
398
407
  docKind,
399
408
  slot: "key",
400
409
  path: resolution.path,
@@ -0,0 +1,42 @@
1
+ import type { AstScalar } from "@telorun/analyzer";
2
+
3
+ /** An `<Alias>.<Name>` scalar split at its first dot — the grammar `kind:`,
4
+ * `extends:`, `x-telo-ref` and `!ref` all share — plus which half the cursor
5
+ * sits on, so the alias navigates to its import and the suffix to the thing
6
+ * the alias qualifies. */
7
+ export interface AliasQualifiedValue {
8
+ /** Undefined for an unqualified value (a bare local name). */
9
+ alias?: string;
10
+ /** Everything after the first dot, or the whole value when unqualified. */
11
+ name: string;
12
+ /** True when the cursor sits on the alias half (dot inclusive). */
13
+ onAlias: boolean;
14
+ }
15
+
16
+ /** Split the scalar at `node` around the cursor at `offset` (a document offset).
17
+ *
18
+ * The parsed value is authoritative where there is one — the source range spans
19
+ * the whole scalar token, so a quoted `x-telo-ref: "Alias.Kind"` would otherwise
20
+ * carry its quotes into the alias and miss every lookup. A `!ref` / `!cel`
21
+ * scalar resolves to a tagged sentinel rather than a string, so those fall back
22
+ * to the raw slice. The range is still what locates the value inside the token,
23
+ * which is what the cursor is hit-tested against. */
24
+ export function splitAliasQualified(
25
+ text: string,
26
+ node: AstScalar,
27
+ offset: number,
28
+ ): AliasQualifiedValue | undefined {
29
+ const raw = text.slice(node.range[0], node.range[1]);
30
+ const value = typeof node.value === "string" ? node.value : raw.trim();
31
+ if (!value) return undefined;
32
+ const at = raw.indexOf(value);
33
+ const start = node.range[0] + (at >= 0 ? at : 0);
34
+
35
+ const dot = value.indexOf(".");
36
+ if (dot === -1) return { name: value, onAlias: false };
37
+ return {
38
+ alias: value.slice(0, dot),
39
+ name: value.slice(dot + 1),
40
+ onAlias: offset <= start + dot,
41
+ };
42
+ }
@@ -1,97 +1,28 @@
1
- import {
2
- parseToAst,
3
- type AstDocument,
4
- type LoadedFile,
5
- type LoadedGraph,
6
- type LoadedModule,
7
- type Range,
8
- } from "@telorun/analyzer";
1
+ import { parseToAst, type AstDocument, type AstScalar, type LoadedGraph } from "@telorun/analyzer";
9
2
  import type { DefinitionResult } from "../types.js";
10
3
  import { resolveNodeAtPosition } from "../completions/resolve-node.js";
4
+ import { splitAliasQualified } from "./alias-qualified-value.js";
5
+ import { moduleForFile } from "./manifest-navigation.js";
6
+ import { resolveCelTarget } from "./resolve-cel-target.js";
7
+ import { isKindSlot, resolveKindTarget } from "./resolve-kind-target.js";
8
+ import { resolveRefTarget } from "./resolve-ref-target.js";
11
9
 
12
- /** The module whose owner or partials includes `filePath`. */
13
- function moduleForFile(graph: LoadedGraph, filePath: string): LoadedModule | undefined {
14
- for (const mod of graph.modules.values()) {
15
- if (mod.owner.source === filePath) return mod;
16
- if (mod.partials.some((p) => p.source === filePath)) return mod;
17
- }
18
- return undefined;
19
- }
20
-
21
- /** First resource named `name` across `files`, located at its `metadata.name`
22
- * (or its first line as a fallback). Names are unique within a module scope, so
23
- * the first hit is the definition. */
24
- function locateResource(files: LoadedFile[], name: string): DefinitionResult | undefined {
25
- for (const file of files) {
26
- for (let i = 0; i < file.manifests.length; i++) {
27
- const manifest = file.manifests[i];
28
- if (!manifest || manifest.metadata?.name !== name) continue;
29
- const pos = file.positions[i];
30
- const range: Range | undefined =
31
- pos?.positionIndex.get("metadata.name") ??
32
- pos?.positionIndex.get("@key:metadata.name") ??
33
- (pos ? { start: { line: pos.sourceLine, character: 0 }, end: { line: pos.sourceLine, character: 0 } } : undefined);
34
- if (range) return { uri: file.source, range };
35
- }
36
- }
37
- return undefined;
38
- }
39
-
40
- /** The `exports.resources` list of a module's owner doc (empty when absent). */
41
- function exportedResources(mod: LoadedModule): string[] {
42
- const doc = mod.owner.manifests.find(
43
- (m) => m?.kind === "Telo.Library" || m?.kind === "Telo.Application",
44
- ) as { exports?: { resources?: unknown } } | undefined;
45
- const list = doc?.exports?.resources;
46
- return Array.isArray(list) ? list.filter((e): e is string => typeof e === "string") : [];
47
- }
48
-
49
- /** Follow `name` into `moduleSource` across the export boundary, honoring the
50
- * `exports.resources` gate and re-export chains (`app → api → domain → …`). A
51
- * terminal match is a locally-owned instance the module actually exports; a
52
- * re-export entry `InnerAlias.name` hops through that module's own import edge.
53
- * `seen` bounds cyclic import graphs. */
54
- function resolveExported(
55
- graph: LoadedGraph,
56
- moduleSource: string,
57
- name: string,
58
- seen: Set<string> = new Set(),
59
- ): DefinitionResult | undefined {
60
- if (seen.has(moduleSource)) return undefined;
61
- seen.add(moduleSource);
62
- const mod = graph.modules.get(moduleSource);
63
- if (!mod) return undefined;
64
-
65
- const exports = exportedResources(mod);
66
-
67
- // No `exports.resources` block → ungated (the module hasn't opted into the
68
- // gate, same as `exports.kinds`): a plain name match keeps navigation working
69
- // for modules that predate explicit exports.
70
- if (exports.length === 0) {
71
- return locateResource([mod.owner, ...mod.partials], name);
72
- }
73
-
74
- if (exports.includes(name)) {
75
- const local = locateResource([mod.owner, ...mod.partials], name);
76
- if (local) return local;
77
- }
78
-
79
- const reexport = exports.find((e) => e.endsWith(`.${name}`));
80
- if (!reexport) return undefined;
81
- const innerAlias = reexport.slice(0, reexport.length - name.length - 1);
82
- const edge = graph.importEdges.get(mod.owner.source)?.get(innerAlias);
83
- return edge ? resolveExported(graph, edge.targetSource, name, seen) : undefined;
84
- }
85
-
86
- /** Resolve the `!ref` under the cursor to its target resource's definition.
10
+ /** Resolve the symbol under the cursor to where it is declared.
87
11
  *
88
- * The ref grammar mirrors `resolveRefSentinels`: the tag's value is split on
89
- * the first dot a bare name (or `Self.name`) is a local resource in the
90
- * current module; `Alias.name` is an exported instance of the module the import
91
- * `Alias` points at, followed transitively through re-exports and gated on each
92
- * module's `exports.resources`. Returns `undefined` when the cursor isn't on a
93
- * `!ref`, or the target can't be found (e.g. a scope-local name, an unexported
94
- * instance, or an import that failed to load). */
12
+ * Three navigable symbol classes, each dispatched on what the cursor sits in
13
+ * rather than on the field it happens to be under:
14
+ *
15
+ * - a CEL identifier (`variables.port`, `resources.Store.conn`) its
16
+ * declaration on the module doc, or the resource it names;
17
+ * - an alias-qualified kind (`kind: Http.Server`, `extends:`, `x-telo-ref`)
18
+ * the `Telo.Definition` that registers it;
19
+ * - a `!ref` target → the resource instance it names.
20
+ *
21
+ * In the last two the alias half (`Http`) navigates to the import that declares
22
+ * it, and the suffix to what the alias qualifies. Returns `undefined` when the
23
+ * cursor is on nothing navigable, or the target can't be found (a kernel
24
+ * built-in, a scope-local name, an unexported entry, or an import that failed
25
+ * to load). */
95
26
  export function buildDefinition(
96
27
  text: string,
97
28
  line: number,
@@ -101,23 +32,22 @@ export function buildDefinition(
101
32
  docs?: AstDocument[],
102
33
  ): DefinitionResult | undefined {
103
34
  const astDocs = docs ?? parseToAst(text);
104
- const node = resolveNodeAtPosition(text, astDocs, line, character)?.node;
105
- if (!node || node.kind !== "scalar" || node.tag !== "!ref") return undefined;
106
-
107
- // The scalar's range covers the ref target text (the value after `!ref`).
108
- const source = text.slice(node.range[0], node.range[1]).trim();
109
- if (!source) return undefined;
35
+ const resolved = resolveNodeAtPosition(text, astDocs, line, character);
36
+ if (!resolved) return undefined;
110
37
 
111
38
  const currentModule = moduleForFile(graph, currentFilePath) ?? graph.entry;
112
- const dot = source.indexOf(".");
113
- const alias = dot === -1 ? undefined : source.slice(0, dot);
114
- const name = dot === -1 ? source : source.slice(dot + 1);
115
39
 
116
- if (alias === undefined || alias === "Self") {
117
- return locateResource([currentModule.owner, ...currentModule.partials], name);
40
+ if (resolved.cel) {
41
+ return resolveCelTarget(graph, currentModule, resolved.cel.segment, resolved.cel.offset);
118
42
  }
119
43
 
120
- const edge = graph.importEdges.get(currentModule.owner.source)?.get(alias);
121
- if (!edge) return undefined;
122
- return resolveExported(graph, edge.targetSource, name);
44
+ const node = resolved.node;
45
+ if (resolved.slot !== "value" || node?.kind !== "scalar") return undefined;
46
+
47
+ const split = splitAliasQualified(text, node as AstScalar, resolved.offset);
48
+ if (!split) return undefined;
49
+
50
+ if (node.tag === "!ref") return resolveRefTarget(graph, currentModule, split);
51
+ if (isKindSlot(resolved)) return resolveKindTarget(graph, currentModule, split);
52
+ return undefined;
123
53
  }
@@ -0,0 +1,124 @@
1
+ import type { LoadedFile, LoadedGraph, LoadedModule, Range } from "@telorun/analyzer";
2
+ import type { DefinitionResult } from "../types.js";
3
+
4
+ /** The shape every navigation lookup reads off a `LoadedFile.manifests` entry.
5
+ * Narrower than `ResourceManifest` on purpose — navigation only ever asks what
6
+ * kind a doc is and what it is named. */
7
+ export interface NavigableManifest {
8
+ kind?: string;
9
+ metadata?: { name?: string };
10
+ }
11
+
12
+ const MODULE_DOC_KINDS = new Set(["Telo.Application", "Telo.Library"]);
13
+ const KIND_DOC_KINDS = new Set(["Telo.Definition", "Telo.Abstract"]);
14
+
15
+ /** The module whose owner or partials includes `filePath`. */
16
+ export function moduleForFile(graph: LoadedGraph, filePath: string): LoadedModule | undefined {
17
+ for (const mod of graph.modules.values()) {
18
+ if (mod.owner.source === filePath) return mod;
19
+ if (mod.partials.some((p) => p.source === filePath)) return mod;
20
+ }
21
+ return undefined;
22
+ }
23
+
24
+ /** Every file in a module's scope, owner first. */
25
+ export function moduleFiles(mod: LoadedModule): LoadedFile[] {
26
+ return [mod.owner, ...mod.partials];
27
+ }
28
+
29
+ function rangeAt(file: LoadedFile, docIndex: number, key: string): Range | undefined {
30
+ return file.positions[docIndex]?.positionIndex.get(key);
31
+ }
32
+
33
+ /** The value span of `path`, falling back to its key when the value has none. */
34
+ function valueRange(file: LoadedFile, docIndex: number, path: string): Range | undefined {
35
+ return rangeAt(file, docIndex, path) ?? rangeAt(file, docIndex, `@key:${path}`);
36
+ }
37
+
38
+ /** The key span of `path` — what a declaration jump underlines, so the target
39
+ * reads as `variables:` / `imports.Http`, not the whole block that follows. */
40
+ function keyRange(file: LoadedFile, docIndex: number, path: string): Range | undefined {
41
+ return rangeAt(file, docIndex, `@key:${path}`) ?? rangeAt(file, docIndex, path);
42
+ }
43
+
44
+ function docStartRange(file: LoadedFile, docIndex: number): Range | undefined {
45
+ const pos = file.positions[docIndex];
46
+ if (!pos) return undefined;
47
+ return {
48
+ start: { line: pos.sourceLine, character: 0 },
49
+ end: { line: pos.sourceLine, character: 0 },
50
+ };
51
+ }
52
+
53
+ /** First manifest across `files` satisfying `match`, located at its
54
+ * `metadata.name` (or its first line as a fallback). Names are unique within a
55
+ * module scope, so the first hit is the definition. */
56
+ export function locateManifest(
57
+ files: LoadedFile[],
58
+ match: (manifest: NavigableManifest) => boolean,
59
+ ): DefinitionResult | undefined {
60
+ for (const file of files) {
61
+ for (let i = 0; i < file.manifests.length; i++) {
62
+ const manifest = file.manifests[i] as NavigableManifest | null;
63
+ if (!manifest || !match(manifest)) continue;
64
+ const range = valueRange(file, i, "metadata.name") ?? docStartRange(file, i);
65
+ if (range) return { uri: file.source, range };
66
+ }
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ /** The resource instance named `name` in a module scope. */
72
+ export function locateResource(files: LoadedFile[], name: string): DefinitionResult | undefined {
73
+ return locateManifest(files, (m) => m.metadata?.name === name);
74
+ }
75
+
76
+ /** The `Telo.Definition` / `Telo.Abstract` doc registering kind suffix `name`.
77
+ * Filtered by doc kind so an instance sharing the name never shadows it. */
78
+ export function locateKindDefinition(
79
+ files: LoadedFile[],
80
+ name: string,
81
+ ): DefinitionResult | undefined {
82
+ return locateManifest(files, (m) => KIND_DOC_KINDS.has(m.kind ?? "") && m.metadata?.name === name);
83
+ }
84
+
85
+ /** Index of the module's `Telo.Application` / `Telo.Library` doc in its owner
86
+ * file — where `imports`, `variables`, `secrets` and `ports` are declared. */
87
+ function moduleDocIndex(mod: LoadedModule): number {
88
+ return mod.owner.manifests.findIndex((m) =>
89
+ MODULE_DOC_KINDS.has((m as NavigableManifest | null)?.kind ?? ""),
90
+ );
91
+ }
92
+
93
+ /** The module doc's JSON projection, for reading its `exports` block. */
94
+ export function moduleDoc(mod: LoadedModule): Record<string, unknown> | undefined {
95
+ const index = moduleDocIndex(mod);
96
+ return index < 0 ? undefined : (mod.owner.manifests[index] as Record<string, unknown> | null) ?? undefined;
97
+ }
98
+
99
+ /** The module's `metadata.name` — the identity a canonical kind (`<module>.<Kind>`)
100
+ * and an `ImportEdge.targetModuleName` are expressed in. */
101
+ export function moduleName(mod: LoadedModule): string | undefined {
102
+ return (moduleDoc(mod) as NavigableManifest | undefined)?.metadata?.name;
103
+ }
104
+
105
+ /** A dotted key path on the module doc (`imports.Http`, `variables.port`),
106
+ * located at the key itself. */
107
+ export function locateModuleDocKey(
108
+ mod: LoadedModule,
109
+ path: string,
110
+ ): DefinitionResult | undefined {
111
+ const index = moduleDocIndex(mod);
112
+ if (index < 0) return undefined;
113
+ const range = keyRange(mod.owner, index, path);
114
+ return range ? { uri: mod.owner.source, range } : undefined;
115
+ }
116
+
117
+ /** Where the import bound to `alias` is declared: the `imports:` map entry, or
118
+ * the legacy standalone `Telo.Import` doc named after the alias. */
119
+ export function locateImport(mod: LoadedModule, alias: string): DefinitionResult | undefined {
120
+ return (
121
+ locateModuleDocKey(mod, `imports.${alias}`) ??
122
+ locateManifest(moduleFiles(mod), (m) => m.kind === "Telo.Import" && m.metadata?.name === alias)
123
+ );
124
+ }