@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.
- package/dist/completions/build.d.ts +2 -2
- package/dist/completions/build.d.ts.map +1 -1
- package/dist/completions/build.js +8 -1
- package/dist/completions/detect-context.d.ts +8 -0
- package/dist/completions/detect-context.d.ts.map +1 -1
- package/dist/completions/detect-context.js +13 -0
- package/dist/completions/import-source.d.ts +17 -0
- package/dist/completions/import-source.d.ts.map +1 -0
- package/dist/completions/import-source.js +146 -0
- package/dist/types.d.ts +32 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/completions/build.ts +11 -3
- package/src/completions/detect-context.ts +24 -1
- package/src/completions/import-source.ts +184 -0
- package/src/types.ts +34 -1
|
@@ -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;
|
|
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;
|
|
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;
|
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,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEpG,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,CAAC;
|
|
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
|
+
"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.
|
|
39
|
+
"@telorun/analyzer": "0.10.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^20.0.0",
|
package/src/completions/build.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|