@telorun/ide-support 0.3.0 → 0.4.1

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.
@@ -1,4 +1,4 @@
1
1
  import type { AnalysisRegistry } from "@telorun/analyzer";
2
- import type { CompletionResult } from "../types.js";
3
- export declare function buildCompletions(text: string, line: number, character: number, registry: AnalysisRegistry | undefined): CompletionResult[];
2
+ import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
3
+ export declare function buildCompletions(text: string, line: number, character: number, registry: AnalysisRegistry | undefined, adapter?: IdeEnvironmentAdapter): Promise<CompletionResult[]>;
4
4
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/completions/build.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA0BpD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,GACrC,gBAAgB,EAAE,CAMpB"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/completions/build.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AA2B3E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAY7B"}
@@ -1,4 +1,5 @@
1
1
  import { detectContext } from "./detect-context.js";
2
+ import { importSourceCompletions } from "./import-source.js";
2
3
  import { propKeyCompletions } from "./prop-keys.js";
3
4
  import { CAPABILITY_VALUES } from "./valid-capabilities.js";
4
5
  function kindCompletions(registry) {
@@ -18,7 +19,7 @@ function capabilityCompletions() {
18
19
  detail: "Telo capability",
19
20
  }));
20
21
  }
21
- export function buildCompletions(text, line, character, registry) {
22
+ export async function buildCompletions(text, line, character, registry, adapter) {
22
23
  const ctx = detectContext(text, line, character);
23
24
  if (!ctx)
24
25
  return [];
@@ -26,5 +27,11 @@ export function buildCompletions(text, line, character, registry) {
26
27
  return kindCompletions(registry);
27
28
  if (ctx.type === "capability")
28
29
  return capabilityCompletions();
30
+ if (ctx.type === "field-value") {
31
+ if (ctx.docKind === "Telo.Import" && ctx.field === "source") {
32
+ return importSourceCompletions(ctx.prefix, ctx.valueStartColumn, adapter);
33
+ }
34
+ return [];
35
+ }
29
36
  return propKeyCompletions(ctx.docKind, ctx.yamlPath, ctx.existingKeys, registry);
30
37
  }
@@ -7,6 +7,14 @@ export type CompletionCtx = {
7
7
  docKind: string;
8
8
  yamlPath: string[];
9
9
  existingKeys: Set<string>;
10
+ } | {
11
+ type: "field-value";
12
+ docKind: string;
13
+ field: string;
14
+ /** Text from the start of the value to the cursor. */
15
+ prefix: string;
16
+ /** 0-based column where the value starts (right after `<field>:` + whitespace). */
17
+ valueStartColumn: number;
10
18
  };
11
19
  export declare function findDocBounds(lines: string[], cursorLine: number): {
12
20
  start: number;
@@ -1 +1 @@
1
- {"version":3,"file":"detect-context.d.ts","sourceRoot":"","sources":["../../src/completions/detect-context.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAEzF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAgBjG;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMlG;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAOxF;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,MAAM,EAAE,CA2BV;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EAAE,EACf,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GACb,GAAG,CAAC,MAAM,CAAC,CAab;AAED,2FAA2F;AAC3F,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,EAAE,GACb,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAgBjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYrG;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,aAAa,GAAG,SAAS,CAuC3B"}
1
+ {"version":3,"file":"detect-context.d.ts","sourceRoot":"","sources":["../../src/completions/detect-context.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GACpF;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEN,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAgBjG;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMlG;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAOxF;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,MAAM,EAAE,CA2BV;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EAAE,EACf,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GACb,GAAG,CAAC,MAAM,CAAC,CAab;AAED,2FAA2F;AAC3F,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,EAAE,GACb,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAgBjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYrG;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,aAAa,GAAG,SAAS,CAqD3B"}
@@ -135,6 +135,19 @@ export function detectContext(text, line, character) {
135
135
  }
136
136
  if (!docKind)
137
137
  return undefined;
138
+ // Field-value completion: `source: <prefix>` on a top-level Telo.Import field.
139
+ // The prefix runs from after `source:`+whitespace up to the cursor — that's
140
+ // what consumers complete against (filesystem paths or registry ids).
141
+ if (docKind === "Telo.Import") {
142
+ const sourceMatch = currentLine.match(/^(source:\s*)(\S*)$/);
143
+ if (sourceMatch) {
144
+ const valueStartColumn = sourceMatch[1].length;
145
+ if (character >= valueStartColumn) {
146
+ const prefix = currentLine.slice(valueStartColumn, character);
147
+ return { type: "field-value", docKind, field: "source", prefix, valueStartColumn };
148
+ }
149
+ }
150
+ }
138
151
  const trimmed = currentLine.trim();
139
152
  // Only trigger when the line looks like a key being typed (or is blank)
140
153
  const isKeyLine = trimmed === "" || /^[a-zA-Z_][a-zA-Z0-9_]*:?$/.test(trimmed);
@@ -0,0 +1,17 @@
1
+ import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
2
+ /**
3
+ * Completions for the `source:` field of a `Telo.Import`.
4
+ *
5
+ * Branches by prefix shape:
6
+ * "" → relative dirs under the manifest dir, plus `./` / `../` seeds.
7
+ * "./..", "../", "/..." → subdirs of the typed path (any subdir; existing manifest gets a hint).
8
+ * "<word>" → registry search by free-text.
9
+ * "<ns>/<name>@<partial>" → version list for that module.
10
+ * "http(s)://", "file://" → no suggestions (opaque URLs).
11
+ *
12
+ * `valueStartColumn` is forwarded onto every result so the host can replace
13
+ * the whole typed value, not just the trailing word (Monaco / VSCode word
14
+ * boundaries don't cross `/` or `@`).
15
+ */
16
+ export declare function importSourceCompletions(prefix: string, valueStartColumn: number, adapter: IdeEnvironmentAdapter | undefined): Promise<CompletionResult[]>;
17
+ //# sourceMappingURL=import-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-source.d.ts","sourceRoot":"","sources":["../../src/completions/import-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAW3E;;;;;;;;;;;;;GAaG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,qBAAqB,GAAG,SAAS,GACzC,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuB7B"}
@@ -0,0 +1,146 @@
1
+ /** Maximum registry hits to surface in a single completion request.
2
+ * Keeps the popover scannable when a broad `q=` query matches the catalog. */
3
+ const REGISTRY_LIMIT = 50;
4
+ /** Caps the number of directory entries we probe with `hasManifest` per
5
+ * request. Each probe is a host-side filesystem stat; the popover would not
6
+ * show more than ~50 entries anyway, so probing further only adds latency. */
7
+ const PATH_PROBE_LIMIT = 50;
8
+ /**
9
+ * Completions for the `source:` field of a `Telo.Import`.
10
+ *
11
+ * Branches by prefix shape:
12
+ * "" → relative dirs under the manifest dir, plus `./` / `../` seeds.
13
+ * "./..", "../", "/..." → subdirs of the typed path (any subdir; existing manifest gets a hint).
14
+ * "<word>" → registry search by free-text.
15
+ * "<ns>/<name>@<partial>" → version list for that module.
16
+ * "http(s)://", "file://" → no suggestions (opaque URLs).
17
+ *
18
+ * `valueStartColumn` is forwarded onto every result so the host can replace
19
+ * the whole typed value, not just the trailing word (Monaco / VSCode word
20
+ * boundaries don't cross `/` or `@`).
21
+ */
22
+ export async function importSourceCompletions(prefix, valueStartColumn, adapter) {
23
+ if (!adapter)
24
+ return [];
25
+ if (prefix.startsWith("http://") ||
26
+ prefix.startsWith("https://") ||
27
+ prefix.startsWith("file://")) {
28
+ return [];
29
+ }
30
+ const isRelativeShape = prefix === "" || prefix.startsWith(".") || prefix.startsWith("/");
31
+ if (isRelativeShape) {
32
+ return relativePathCompletions(prefix, valueStartColumn, adapter);
33
+ }
34
+ const atIdx = prefix.indexOf("@");
35
+ if (atIdx > 0) {
36
+ return versionCompletions(prefix, atIdx, valueStartColumn, adapter);
37
+ }
38
+ return registrySearchCompletions(prefix, valueStartColumn, adapter);
39
+ }
40
+ async function relativePathCompletions(prefix, valueStartColumn, adapter) {
41
+ // Empty prefix → seed `./` and `../` so the user gets traction; otherwise
42
+ // we'd return an unfiltered dump of the manifest directory which is rarely
43
+ // what the user wants for a `Telo.Import`.
44
+ if (prefix === "") {
45
+ return [
46
+ {
47
+ label: "./",
48
+ kind: "folder",
49
+ insertText: "./",
50
+ sortText: "0_./",
51
+ replaceFromColumn: valueStartColumn,
52
+ },
53
+ {
54
+ label: "../",
55
+ kind: "folder",
56
+ insertText: "../",
57
+ sortText: "0_../",
58
+ replaceFromColumn: valueStartColumn,
59
+ },
60
+ ];
61
+ }
62
+ // Split the typed prefix at the last `/`: everything up to it is the
63
+ // directory we list, the trailing chunk is what the user is filtering on.
64
+ const lastSlash = prefix.lastIndexOf("/");
65
+ const dirPart = lastSlash >= 0 ? prefix.slice(0, lastSlash + 1) : "";
66
+ const namePart = lastSlash >= 0 ? prefix.slice(lastSlash + 1) : prefix;
67
+ // If the user hasn't typed a slash yet (e.g. just `.` or `..`), nothing
68
+ // to list — let them keep typing until they pass `/`.
69
+ if (dirPart === "")
70
+ return [];
71
+ const dirs = await adapter.listDirectories(dirPart);
72
+ const matches = dirs
73
+ .filter((name) => name.startsWith(namePart))
74
+ .sort()
75
+ .slice(0, PATH_PROBE_LIMIT);
76
+ // Probe every candidate in parallel. Sequential `await` here makes a wide
77
+ // directory (e.g. `modules/` with dozens of children) feel sluggish — Promise.all
78
+ // fans the host's filesystem stats out concurrently so total latency is bounded
79
+ // by the slowest single probe rather than their sum.
80
+ return Promise.all(matches.map(async (name) => {
81
+ const fullPath = dirPart + name;
82
+ const isModule = await adapter.hasManifest(fullPath);
83
+ return {
84
+ label: name,
85
+ kind: "folder",
86
+ detail: isModule ? "telo module" : "folder",
87
+ insertText: fullPath,
88
+ filterText: fullPath,
89
+ replaceFromColumn: valueStartColumn,
90
+ // Modules sort above plain folders so they surface first when the user
91
+ // is browsing a `modules/` tree mixed with non-Telo siblings.
92
+ sortText: isModule ? `0_${name}` : `1_${name}`,
93
+ };
94
+ }));
95
+ }
96
+ async function registrySearchCompletions(prefix, valueStartColumn, adapter) {
97
+ // The registry's `q` filter ILIKEs against name / namespace / description —
98
+ // it doesn't know about the `<namespace>/<name>` shape. Once the user has
99
+ // typed a `/`, sending the literal `std/htt` as `q` matches nothing because
100
+ // the slash is not in any of those columns. Split here so `q` carries just
101
+ // the bit that looks like a name, and apply the namespace constraint
102
+ // client-side.
103
+ const slashIdx = prefix.indexOf("/");
104
+ const namespacePart = slashIdx >= 0 ? prefix.slice(0, slashIdx) : "";
105
+ const namePart = slashIdx >= 0 ? prefix.slice(slashIdx + 1) : prefix;
106
+ const hits = await adapter.searchRegistry(namePart);
107
+ const filtered = namespacePart
108
+ ? hits.filter((h) => h.namespace.startsWith(namespacePart))
109
+ : hits;
110
+ return filtered.slice(0, REGISTRY_LIMIT).map((m) => {
111
+ const id = `${m.namespace}/${m.name}@${m.version}`;
112
+ return {
113
+ label: id,
114
+ kind: "module",
115
+ detail: m.description ?? "registry module",
116
+ insertText: id,
117
+ filterText: id,
118
+ replaceFromColumn: valueStartColumn,
119
+ };
120
+ });
121
+ }
122
+ async function versionCompletions(prefix, atIdx, valueStartColumn, adapter) {
123
+ const beforeAt = prefix.slice(0, atIdx);
124
+ const partialVersion = prefix.slice(atIdx + 1);
125
+ const slashIdx = beforeAt.indexOf("/");
126
+ if (slashIdx <= 0 || slashIdx === beforeAt.length - 1)
127
+ return [];
128
+ const namespace = beforeAt.slice(0, slashIdx);
129
+ const name = beforeAt.slice(slashIdx + 1);
130
+ const versions = await adapter.listRegistryVersions(namespace, name);
131
+ const matches = versions.filter((v) => v.startsWith(partialVersion));
132
+ return matches.map((version, idx) => {
133
+ const id = `${namespace}/${name}@${version}`;
134
+ return {
135
+ label: id,
136
+ kind: "value",
137
+ detail: idx === 0 ? "latest" : `v${version}`,
138
+ insertText: id,
139
+ filterText: id,
140
+ replaceFromColumn: valueStartColumn,
141
+ // Preserve registry's ordering (newest first) so the latest version is
142
+ // suggested at the top regardless of lexical comparison.
143
+ sortText: String(idx).padStart(4, "0"),
144
+ };
145
+ });
146
+ }
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { AnalysisRegistry, DiagnosticSeverity } from "@telorun/analyzer";
2
2
  export type { Position, Range, AnalysisDiagnostic, PositionIndex, } from "@telorun/analyzer";
3
3
  import type { AnalysisRegistry, DiagnosticSeverity, PositionIndex, Range } from "@telorun/analyzer";
4
- export type CompletionKind = "class" | "enumMember" | "property";
4
+ export type CompletionKind = "class" | "enumMember" | "property" | "folder" | "module" | "value";
5
5
  export interface CompletionResult {
6
6
  label: string;
7
7
  kind: CompletionKind;
@@ -11,6 +11,37 @@ export interface CompletionResult {
11
11
  snippet?: boolean;
12
12
  preselect?: boolean;
13
13
  sortText?: string;
14
+ 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
+ }
21
+ export interface RegistryModule {
22
+ namespace: string;
23
+ name: string;
24
+ version: string;
25
+ description?: string;
26
+ }
27
+ /** Host-supplied bridge that lets ide-support reach the filesystem and the
28
+ * module registry without depending on Node, Tauri, or vscode APIs. Each
29
+ * host (VSCode extension, Telo editor) builds an adapter scoped to the
30
+ * currently-edited manifest before calling `buildCompletions`. */
31
+ export interface IdeEnvironmentAdapter {
32
+ /** Subdirectory names within `relPath` (resolved against the manifest's
33
+ * directory). Returns [] if the path doesn't exist or isn't a directory.
34
+ * Never throws — hosts swallow ENOENT and similar. */
35
+ listDirectories(relPath: string): Promise<string[]>;
36
+ /** True iff `<relPath>/telo.yaml` exists relative to the manifest dir.
37
+ * Used to mark directories that are valid `Telo.Import` targets. */
38
+ hasManifest(relPath: string): Promise<boolean>;
39
+ /** Free-text search against the configured module registry. Matches against
40
+ * name, namespace, and description. Empty `query` should return the full
41
+ * (capped) catalog. */
42
+ searchRegistry(query: string): Promise<RegistryModule[]>;
43
+ /** All published versions for a module, newest first. */
44
+ listRegistryVersions(namespace: string, name: string): Promise<string[]>;
14
45
  }
15
46
  export interface NormalizedDiagnostic {
16
47
  range: Range;
@@ -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,CAAC;AAEjE,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;CACnB;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":"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,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;mEAGmE;AACnE,MAAM,WAAW,qBAAqB;IACpC;;2DAEuD;IACvD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpD;yEACqE;IACrE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C;;4BAEwB;IACxB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IACzD,yDAAyD;IACzD,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC1E;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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/ide-support",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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.9.0"
39
+ "@telorun/analyzer": "0.10.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^20.0.0",
@@ -1,6 +1,7 @@
1
1
  import type { AnalysisRegistry } from "@telorun/analyzer";
2
- import type { CompletionResult } from "../types.js";
2
+ import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
3
3
  import { detectContext } from "./detect-context.js";
4
+ import { importSourceCompletions } from "./import-source.js";
4
5
  import { propKeyCompletions } from "./prop-keys.js";
5
6
  import { CAPABILITY_VALUES } from "./valid-capabilities.js";
6
7
 
@@ -25,15 +26,22 @@ function capabilityCompletions(): CompletionResult[] {
25
26
  }));
26
27
  }
27
28
 
28
- export function buildCompletions(
29
+ export async function buildCompletions(
29
30
  text: string,
30
31
  line: number,
31
32
  character: number,
32
33
  registry: AnalysisRegistry | undefined,
33
- ): CompletionResult[] {
34
+ adapter?: IdeEnvironmentAdapter,
35
+ ): Promise<CompletionResult[]> {
34
36
  const ctx = detectContext(text, line, character);
35
37
  if (!ctx) return [];
36
38
  if (ctx.type === "kind") return kindCompletions(registry);
37
39
  if (ctx.type === "capability") return capabilityCompletions();
40
+ if (ctx.type === "field-value") {
41
+ if (ctx.docKind === "Telo.Import" && ctx.field === "source") {
42
+ return importSourceCompletions(ctx.prefix, ctx.valueStartColumn, adapter);
43
+ }
44
+ return [];
45
+ }
38
46
  return propKeyCompletions(ctx.docKind, ctx.yamlPath, ctx.existingKeys, registry);
39
47
  }
@@ -1,7 +1,16 @@
1
1
  export type CompletionCtx =
2
2
  | { type: "kind" }
3
3
  | { type: "capability" }
4
- | { type: "prop-key"; docKind: string; yamlPath: string[]; existingKeys: Set<string> };
4
+ | { type: "prop-key"; docKind: string; yamlPath: string[]; existingKeys: Set<string> }
5
+ | {
6
+ type: "field-value";
7
+ docKind: string;
8
+ field: string;
9
+ /** Text from the start of the value to the cursor. */
10
+ prefix: string;
11
+ /** 0-based column where the value starts (right after `<field>:` + whitespace). */
12
+ valueStartColumn: number;
13
+ };
5
14
 
6
15
  export function findDocBounds(lines: string[], cursorLine: number): { start: number; end: number } {
7
16
  let start = 0;
@@ -162,6 +171,20 @@ export function detectContext(
162
171
 
163
172
  if (!docKind) return undefined;
164
173
 
174
+ // Field-value completion: `source: <prefix>` on a top-level Telo.Import field.
175
+ // The prefix runs from after `source:`+whitespace up to the cursor — that's
176
+ // what consumers complete against (filesystem paths or registry ids).
177
+ if (docKind === "Telo.Import") {
178
+ const sourceMatch = currentLine.match(/^(source:\s*)(\S*)$/);
179
+ if (sourceMatch) {
180
+ const valueStartColumn = sourceMatch[1].length;
181
+ if (character >= valueStartColumn) {
182
+ const prefix = currentLine.slice(valueStartColumn, character);
183
+ return { type: "field-value", docKind, field: "source", prefix, valueStartColumn };
184
+ }
185
+ }
186
+ }
187
+
165
188
  const trimmed = currentLine.trim();
166
189
 
167
190
  // Only trigger when the line looks like a key being typed (or is blank)
@@ -0,0 +1,184 @@
1
+ import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
2
+
3
+ /** Maximum registry hits to surface in a single completion request.
4
+ * Keeps the popover scannable when a broad `q=` query matches the catalog. */
5
+ const REGISTRY_LIMIT = 50;
6
+
7
+ /** Caps the number of directory entries we probe with `hasManifest` per
8
+ * request. Each probe is a host-side filesystem stat; the popover would not
9
+ * show more than ~50 entries anyway, so probing further only adds latency. */
10
+ const PATH_PROBE_LIMIT = 50;
11
+
12
+ /**
13
+ * Completions for the `source:` field of a `Telo.Import`.
14
+ *
15
+ * Branches by prefix shape:
16
+ * "" → relative dirs under the manifest dir, plus `./` / `../` seeds.
17
+ * "./..", "../", "/..." → subdirs of the typed path (any subdir; existing manifest gets a hint).
18
+ * "<word>" → registry search by free-text.
19
+ * "<ns>/<name>@<partial>" → version list for that module.
20
+ * "http(s)://", "file://" → no suggestions (opaque URLs).
21
+ *
22
+ * `valueStartColumn` is forwarded onto every result so the host can replace
23
+ * the whole typed value, not just the trailing word (Monaco / VSCode word
24
+ * boundaries don't cross `/` or `@`).
25
+ */
26
+ export async function importSourceCompletions(
27
+ prefix: string,
28
+ valueStartColumn: number,
29
+ adapter: IdeEnvironmentAdapter | undefined,
30
+ ): Promise<CompletionResult[]> {
31
+ if (!adapter) return [];
32
+
33
+ if (
34
+ prefix.startsWith("http://") ||
35
+ prefix.startsWith("https://") ||
36
+ prefix.startsWith("file://")
37
+ ) {
38
+ return [];
39
+ }
40
+
41
+ const isRelativeShape =
42
+ prefix === "" || prefix.startsWith(".") || prefix.startsWith("/");
43
+ if (isRelativeShape) {
44
+ return relativePathCompletions(prefix, valueStartColumn, adapter);
45
+ }
46
+
47
+ const atIdx = prefix.indexOf("@");
48
+ if (atIdx > 0) {
49
+ return versionCompletions(prefix, atIdx, valueStartColumn, adapter);
50
+ }
51
+
52
+ return registrySearchCompletions(prefix, valueStartColumn, adapter);
53
+ }
54
+
55
+ async function relativePathCompletions(
56
+ prefix: string,
57
+ valueStartColumn: number,
58
+ adapter: IdeEnvironmentAdapter,
59
+ ): Promise<CompletionResult[]> {
60
+ // Empty prefix → seed `./` and `../` so the user gets traction; otherwise
61
+ // we'd return an unfiltered dump of the manifest directory which is rarely
62
+ // what the user wants for a `Telo.Import`.
63
+ if (prefix === "") {
64
+ return [
65
+ {
66
+ label: "./",
67
+ kind: "folder",
68
+ insertText: "./",
69
+ sortText: "0_./",
70
+ replaceFromColumn: valueStartColumn,
71
+ },
72
+ {
73
+ label: "../",
74
+ kind: "folder",
75
+ insertText: "../",
76
+ sortText: "0_../",
77
+ replaceFromColumn: valueStartColumn,
78
+ },
79
+ ];
80
+ }
81
+
82
+ // Split the typed prefix at the last `/`: everything up to it is the
83
+ // directory we list, the trailing chunk is what the user is filtering on.
84
+ const lastSlash = prefix.lastIndexOf("/");
85
+ const dirPart = lastSlash >= 0 ? prefix.slice(0, lastSlash + 1) : "";
86
+ const namePart = lastSlash >= 0 ? prefix.slice(lastSlash + 1) : prefix;
87
+
88
+ // If the user hasn't typed a slash yet (e.g. just `.` or `..`), nothing
89
+ // to list — let them keep typing until they pass `/`.
90
+ if (dirPart === "") return [];
91
+
92
+ const dirs = await adapter.listDirectories(dirPart);
93
+ const matches = dirs
94
+ .filter((name) => name.startsWith(namePart))
95
+ .sort()
96
+ .slice(0, PATH_PROBE_LIMIT);
97
+
98
+ // Probe every candidate in parallel. Sequential `await` here makes a wide
99
+ // directory (e.g. `modules/` with dozens of children) feel sluggish — Promise.all
100
+ // fans the host's filesystem stats out concurrently so total latency is bounded
101
+ // by the slowest single probe rather than their sum.
102
+ return Promise.all(
103
+ matches.map(async (name) => {
104
+ const fullPath = dirPart + name;
105
+ const isModule = await adapter.hasManifest(fullPath);
106
+ return {
107
+ label: name,
108
+ kind: "folder",
109
+ detail: isModule ? "telo module" : "folder",
110
+ insertText: fullPath,
111
+ filterText: fullPath,
112
+ replaceFromColumn: valueStartColumn,
113
+ // Modules sort above plain folders so they surface first when the user
114
+ // is browsing a `modules/` tree mixed with non-Telo siblings.
115
+ sortText: isModule ? `0_${name}` : `1_${name}`,
116
+ } satisfies CompletionResult;
117
+ }),
118
+ );
119
+ }
120
+
121
+ async function registrySearchCompletions(
122
+ prefix: string,
123
+ valueStartColumn: number,
124
+ adapter: IdeEnvironmentAdapter,
125
+ ): Promise<CompletionResult[]> {
126
+ // The registry's `q` filter ILIKEs against name / namespace / description —
127
+ // it doesn't know about the `<namespace>/<name>` shape. Once the user has
128
+ // typed a `/`, sending the literal `std/htt` as `q` matches nothing because
129
+ // the slash is not in any of those columns. Split here so `q` carries just
130
+ // the bit that looks like a name, and apply the namespace constraint
131
+ // client-side.
132
+ const slashIdx = prefix.indexOf("/");
133
+ const namespacePart = slashIdx >= 0 ? prefix.slice(0, slashIdx) : "";
134
+ const namePart = slashIdx >= 0 ? prefix.slice(slashIdx + 1) : prefix;
135
+
136
+ const hits = await adapter.searchRegistry(namePart);
137
+ const filtered = namespacePart
138
+ ? hits.filter((h) => h.namespace.startsWith(namespacePart))
139
+ : hits;
140
+
141
+ return filtered.slice(0, REGISTRY_LIMIT).map((m) => {
142
+ const id = `${m.namespace}/${m.name}@${m.version}`;
143
+ return {
144
+ label: id,
145
+ kind: "module",
146
+ detail: m.description ?? "registry module",
147
+ insertText: id,
148
+ filterText: id,
149
+ replaceFromColumn: valueStartColumn,
150
+ };
151
+ });
152
+ }
153
+
154
+ async function versionCompletions(
155
+ prefix: string,
156
+ atIdx: number,
157
+ valueStartColumn: number,
158
+ adapter: IdeEnvironmentAdapter,
159
+ ): Promise<CompletionResult[]> {
160
+ const beforeAt = prefix.slice(0, atIdx);
161
+ const partialVersion = prefix.slice(atIdx + 1);
162
+ const slashIdx = beforeAt.indexOf("/");
163
+ if (slashIdx <= 0 || slashIdx === beforeAt.length - 1) return [];
164
+
165
+ const namespace = beforeAt.slice(0, slashIdx);
166
+ const name = beforeAt.slice(slashIdx + 1);
167
+ const versions = await adapter.listRegistryVersions(namespace, name);
168
+
169
+ const matches = versions.filter((v) => v.startsWith(partialVersion));
170
+ return matches.map((version, idx) => {
171
+ const id = `${namespace}/${name}@${version}`;
172
+ return {
173
+ label: id,
174
+ kind: "value",
175
+ detail: idx === 0 ? "latest" : `v${version}`,
176
+ insertText: id,
177
+ filterText: id,
178
+ replaceFromColumn: valueStartColumn,
179
+ // Preserve registry's ordering (newest first) so the latest version is
180
+ // suggested at the top regardless of lexical comparison.
181
+ sortText: String(idx).padStart(4, "0"),
182
+ };
183
+ });
184
+ }
package/src/types.ts CHANGED
@@ -12,7 +12,7 @@ export type {
12
12
 
13
13
  import type { AnalysisRegistry, DiagnosticSeverity, PositionIndex, Range } from "@telorun/analyzer";
14
14
 
15
- export type CompletionKind = "class" | "enumMember" | "property";
15
+ export type CompletionKind = "class" | "enumMember" | "property" | "folder" | "module" | "value";
16
16
 
17
17
  export interface CompletionResult {
18
18
  label: string;
@@ -23,6 +23,39 @@ export interface CompletionResult {
23
23
  snippet?: boolean;
24
24
  preselect?: boolean;
25
25
  sortText?: string;
26
+ filterText?: string;
27
+ /** When set, the host should replace text from this 0-based column on the
28
+ * cursor's line up to the cursor. Required when the completion value
29
+ * contains non-word characters (`/`, `@`, `.`) that the host's default
30
+ * word boundary would not include in the replaced range. */
31
+ replaceFromColumn?: number;
32
+ }
33
+
34
+ export interface RegistryModule {
35
+ namespace: string;
36
+ name: string;
37
+ version: string;
38
+ description?: string;
39
+ }
40
+
41
+ /** Host-supplied bridge that lets ide-support reach the filesystem and the
42
+ * module registry without depending on Node, Tauri, or vscode APIs. Each
43
+ * host (VSCode extension, Telo editor) builds an adapter scoped to the
44
+ * currently-edited manifest before calling `buildCompletions`. */
45
+ export interface IdeEnvironmentAdapter {
46
+ /** Subdirectory names within `relPath` (resolved against the manifest's
47
+ * directory). Returns [] if the path doesn't exist or isn't a directory.
48
+ * Never throws — hosts swallow ENOENT and similar. */
49
+ listDirectories(relPath: string): Promise<string[]>;
50
+ /** True iff `<relPath>/telo.yaml` exists relative to the manifest dir.
51
+ * Used to mark directories that are valid `Telo.Import` targets. */
52
+ hasManifest(relPath: string): Promise<boolean>;
53
+ /** Free-text search against the configured module registry. Matches against
54
+ * name, namespace, and description. Empty `query` should return the full
55
+ * (capped) catalog. */
56
+ searchRegistry(query: string): Promise<RegistryModule[]>;
57
+ /** All published versions for a module, newest first. */
58
+ listRegistryVersions(namespace: string, name: string): Promise<string[]>;
26
59
  }
27
60
 
28
61
  export interface NormalizedDiagnostic {