config: complete virtual tree editor contract
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"@sveltejs/adapter-static": "npm:@sveltejs/adapter-static@3.0.9",
|
||||
"@sveltejs/kit": "npm:@sveltejs/kit@2.49.4",
|
||||
"@sveltejs/vite-plugin-svelte": "npm:@sveltejs/vite-plugin-svelte@6.2.1",
|
||||
"@codemirror/autocomplete": "npm:@codemirror/autocomplete@6.20.0",
|
||||
"@codemirror/state": "npm:@codemirror/state@6.7.1",
|
||||
"@codemirror/view": "npm:@codemirror/view@6.43.8",
|
||||
"decodal-codemirror": "npm:decodal-codemirror@0.1.6",
|
||||
|
||||
Generated
+12
@@ -3,6 +3,7 @@
|
||||
"specifiers": {
|
||||
"jsr:@std/assert@*": "1.0.19",
|
||||
"jsr:@std/internal@^1.0.12": "1.0.14",
|
||||
"npm:@codemirror/autocomplete@6.20.0": "6.20.0",
|
||||
"npm:@codemirror/state@6.7.1": "6.7.1",
|
||||
"npm:@codemirror/view@6.43.8": "6.43.8",
|
||||
"npm:@fontsource/ibm-plex-mono@5.3.0": "5.3.0",
|
||||
@@ -34,9 +35,19 @@
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"@codemirror/autocomplete@6.20.0": {
|
||||
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
|
||||
"dependencies": [
|
||||
"@codemirror/language",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common"
|
||||
]
|
||||
},
|
||||
"@codemirror/language@6.12.4": {
|
||||
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||
"dependencies": [
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
@@ -988,6 +999,7 @@
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"npm:@codemirror/autocomplete@6.20.0",
|
||||
"npm:@codemirror/state@6.7.1",
|
||||
"npm:@codemirror/view@6.43.8",
|
||||
"npm:@sveltejs/adapter-static@3.0.9",
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
let diagnostics = $state<ConfigDiagnostic[]>([]);
|
||||
let status = $state("Loading source tree…");
|
||||
let busy = $state(false);
|
||||
let draftChanges = $state<ConfigTreeChange[]>([]);
|
||||
let baseRevision = $state(0);
|
||||
let baseDigest = $state("");
|
||||
let renamePath = $state("");
|
||||
let toolchain: ConfigSourceToolchain | null = null;
|
||||
|
||||
const paths = $derived(
|
||||
@@ -29,7 +33,7 @@
|
||||
const selected = $derived(
|
||||
treeState && selectedPath ? treeState.snapshot.entries[selectedPath] : undefined,
|
||||
);
|
||||
const dirty = $derived(selected ? source !== selected.content : source.length > 0);
|
||||
const dirty = $derived(draftChanges.length > 0 || (selected ? source !== selected.content : source.length > 0));
|
||||
|
||||
onMount(() => {
|
||||
toolchain = new ConfigSourceToolchain();
|
||||
@@ -44,6 +48,11 @@
|
||||
selectedPath = Object.keys(treeState.snapshot.entries).toSorted()[0] ?? "";
|
||||
}
|
||||
source = selectedPath ? treeState.snapshot.entries[selectedPath].content : "";
|
||||
baseRevision = treeState.snapshot.revision;
|
||||
baseDigest = treeState.snapshot.digest;
|
||||
await toolchain?.setSnapshot(treeState.snapshot);
|
||||
draftChanges = [];
|
||||
renamePath = selectedPath;
|
||||
diagnostics = [];
|
||||
status = treeState.snapshot.revision === 0
|
||||
? "No committed sources yet. Create workspace.dcdl to begin."
|
||||
@@ -53,9 +62,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
function select(path: string) {
|
||||
async function stageCurrent() {
|
||||
const change = currentChange();
|
||||
if (!change || !toolchain) return;
|
||||
draftChanges = [...draftChanges.filter((item) => !changeTouches(item, selectedPath)), change];
|
||||
const candidate = await toolchain.applyChanges([change]);
|
||||
if (treeState) treeState = { ...treeState, snapshot: candidate };
|
||||
}
|
||||
|
||||
function changeTouches(change: ConfigTreeChange, path: string): boolean {
|
||||
return change.kind === "rename" ? change.from === path || change.to === path : change.path === path;
|
||||
}
|
||||
|
||||
async function select(path: string) {
|
||||
await stageCurrent();
|
||||
selectedPath = path;
|
||||
source = treeState?.snapshot.entries[path]?.content ?? "";
|
||||
renamePath = path;
|
||||
diagnostics = [];
|
||||
}
|
||||
|
||||
@@ -81,7 +104,9 @@
|
||||
|
||||
function entrypoints(): string[] {
|
||||
if (!treeState) return [];
|
||||
if (treeState.contract.entrypoints.length > 0) return treeState.contract.entrypoints;
|
||||
const known = new Set(Object.keys(treeState.snapshot.entries));
|
||||
const configured = treeState.contract.entrypoints.filter((path) => known.has(path));
|
||||
if (configured.length > 0) return configured;
|
||||
if (treeState.snapshot.entries["workspace.dcdl"] || selectedPath === "workspace.dcdl") {
|
||||
return ["workspace.dcdl"];
|
||||
}
|
||||
@@ -90,7 +115,7 @@
|
||||
|
||||
async function analyze() {
|
||||
if (!toolchain || !treeState || !selectedPath) return;
|
||||
diagnostics = await toolchain.analyze(treeState.snapshot, selectedPath, source);
|
||||
diagnostics = await toolchain.analyze(selectedPath, source);
|
||||
status = diagnostics.length === 0 ? "No diagnostics." : `${diagnostics.length} diagnostic(s).`;
|
||||
}
|
||||
|
||||
@@ -106,16 +131,17 @@
|
||||
|
||||
async function preview() {
|
||||
if (!treeState) return;
|
||||
const change = currentChange();
|
||||
if (!change) {
|
||||
await stageCurrent();
|
||||
if (draftChanges.length === 0) {
|
||||
status = "No draft changes to preview.";
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
try {
|
||||
const candidate = await previewConfigTree(workspaceId, {
|
||||
changes: [change],
|
||||
changes: draftChanges,
|
||||
entrypoints: entrypoints(),
|
||||
toolchain_fingerprint: treeState.contract.fingerprint,
|
||||
});
|
||||
diagnostics = [];
|
||||
status = `Preview valid · projection ${candidate.evaluation.projection_digest.slice(0, 20)}…`;
|
||||
@@ -128,19 +154,24 @@
|
||||
|
||||
async function commit() {
|
||||
if (!treeState) return;
|
||||
const change = currentChange();
|
||||
if (!change) {
|
||||
await stageCurrent();
|
||||
if (draftChanges.length === 0) {
|
||||
status = "No draft changes to commit.";
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
try {
|
||||
treeState = await commitConfigTree(workspaceId, {
|
||||
base_revision: treeState.snapshot.revision,
|
||||
base_digest: treeState.snapshot.digest,
|
||||
changes: [change],
|
||||
base_revision: baseRevision,
|
||||
base_digest: baseDigest,
|
||||
changes: draftChanges,
|
||||
entrypoints: entrypoints(),
|
||||
toolchain_fingerprint: treeState.contract.fingerprint,
|
||||
});
|
||||
draftChanges = [];
|
||||
baseRevision = treeState.snapshot.revision;
|
||||
baseDigest = treeState.snapshot.digest;
|
||||
await toolchain?.setSnapshot(treeState.snapshot);
|
||||
source = treeState.snapshot.entries[selectedPath]?.content ?? "";
|
||||
diagnostics = [];
|
||||
status = `Committed revision ${treeState.snapshot.revision}.`;
|
||||
@@ -161,29 +192,38 @@
|
||||
}
|
||||
|
||||
async function deleteEntry() {
|
||||
if (!treeState || !selected) return;
|
||||
busy = true;
|
||||
try {
|
||||
const remainingEntrypoints = treeState.contract.entrypoints.filter((path) => path !== selectedPath);
|
||||
treeState = await commitConfigTree(workspaceId, {
|
||||
base_revision: treeState.snapshot.revision,
|
||||
base_digest: treeState.snapshot.digest,
|
||||
changes: [{
|
||||
kind: "delete",
|
||||
path: selectedPath,
|
||||
expected_digest: selected.content_digest,
|
||||
}],
|
||||
entrypoints: remainingEntrypoints,
|
||||
});
|
||||
selectedPath = Object.keys(treeState.snapshot.entries).toSorted()[0] ?? "";
|
||||
source = selectedPath ? treeState.snapshot.entries[selectedPath].content : "";
|
||||
diagnostics = [];
|
||||
status = `Committed revision ${treeState.snapshot.revision}.`;
|
||||
} catch (error) {
|
||||
status = String(error);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
if (!treeState || !selected || !toolchain) return;
|
||||
const change: ConfigTreeChange = {
|
||||
kind: "delete",
|
||||
path: selectedPath,
|
||||
expected_digest: selected.content_digest,
|
||||
};
|
||||
draftChanges = [...draftChanges.filter((item) => !changeTouches(item, selectedPath)), change];
|
||||
const candidate = await toolchain.applyChanges([change]);
|
||||
treeState = { ...treeState, snapshot: candidate };
|
||||
selectedPath = Object.keys(candidate.entries).toSorted()[0] ?? "";
|
||||
source = selectedPath ? candidate.entries[selectedPath].content : "";
|
||||
renamePath = selectedPath;
|
||||
status = "Delete staged. Preview and Commit to persist the candidate tree.";
|
||||
}
|
||||
|
||||
async function renameEntry() {
|
||||
if (!treeState || !selected || !toolchain) return;
|
||||
const to = renamePath.trim();
|
||||
if (!to || to === selectedPath) return;
|
||||
await stageCurrent();
|
||||
const change: ConfigTreeChange = {
|
||||
kind: "rename",
|
||||
from: selectedPath,
|
||||
to,
|
||||
expected_digest: selected.content_digest,
|
||||
};
|
||||
draftChanges = [...draftChanges.filter((item) => !changeTouches(item, selectedPath)), change];
|
||||
const candidate = await toolchain.applyChanges([change]);
|
||||
treeState = { ...treeState, snapshot: candidate };
|
||||
selectedPath = to;
|
||||
source = candidate.entries[to]?.content ?? "";
|
||||
status = `Rename to ${to} staged. Preview and Commit to persist.`;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -216,6 +256,8 @@
|
||||
<strong>{selectedPath || "Select or create a source"}</strong>
|
||||
</div>
|
||||
<div class="config-source-actions">
|
||||
<input aria-label="Rename path" bind:value={renamePath} disabled={!selected || busy} />
|
||||
<button type="button" onclick={renameEntry} disabled={!selected || renamePath === selectedPath || busy}>Rename</button>
|
||||
<button type="button" onclick={format} disabled={!selectedPath || busy}>Format</button>
|
||||
<button type="button" onclick={analyze} disabled={!selectedPath || busy}>Analyze</button>
|
||||
<button type="button" onclick={preview} disabled={!dirty || busy}>Preview</button>
|
||||
@@ -223,7 +265,12 @@
|
||||
<button class="danger" type="button" onclick={deleteEntry} disabled={!selected || busy}>Delete</button>
|
||||
</div>
|
||||
</header>
|
||||
<DecodalSourceEditor value={source} readonly={!selectedPath || busy} onChange={(value) => source = value} />
|
||||
<DecodalSourceEditor
|
||||
value={source}
|
||||
readonly={!selectedPath || busy}
|
||||
onChange={(value) => source = value}
|
||||
onComplete={(value, offset, explicit) => toolchain?.complete(selectedPath, value, offset, explicit) ?? Promise.resolve(null)}
|
||||
/>
|
||||
<p class="config-source-status" aria-live="polite">{status}</p>
|
||||
{#if diagnostics.length > 0}
|
||||
<ol class="config-source-diagnostics">
|
||||
|
||||
+12
@@ -3,19 +3,31 @@
|
||||
|
||||
export function analyze_snapshot(snapshot: any, entrypoint: string, source_override?: string | null): any;
|
||||
|
||||
export function apply_changes(changes: any): any;
|
||||
|
||||
export function complete_current(entrypoint: string, source: string, utf8_byte_offset: number, explicit: boolean): any;
|
||||
|
||||
export function evaluate_current(contract: any): any;
|
||||
|
||||
export function evaluate_snapshot(snapshot: any, contract: any): any;
|
||||
|
||||
export function formatSource(source: string): string;
|
||||
|
||||
export function format_source(source: string): string;
|
||||
|
||||
export function set_snapshot(snapshot: any): void;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly analyze_snapshot: (a: any, b: number, c: number, d: number, e: number) => [number, number, number];
|
||||
readonly apply_changes: (a: any) => [number, number, number];
|
||||
readonly complete_current: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
|
||||
readonly evaluate_current: (a: any) => [number, number, number];
|
||||
readonly evaluate_snapshot: (a: any, b: any) => [number, number, number];
|
||||
readonly format_source: (a: number, b: number) => [number, number, number, number];
|
||||
readonly set_snapshot: (a: any) => [number, number];
|
||||
readonly formatSource: (a: number, b: number) => [number, number];
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
|
||||
@@ -18,6 +18,49 @@ export function analyze_snapshot(snapshot, entrypoint, source_override) {
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} changes
|
||||
* @returns {any}
|
||||
*/
|
||||
export function apply_changes(changes) {
|
||||
const ret = wasm.apply_changes(changes);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} entrypoint
|
||||
* @param {string} source
|
||||
* @param {number} utf8_byte_offset
|
||||
* @param {boolean} explicit
|
||||
* @returns {any}
|
||||
*/
|
||||
export function complete_current(entrypoint, source, utf8_byte_offset, explicit) {
|
||||
const ptr0 = passStringToWasm0(entrypoint, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.complete_current(ptr0, len0, ptr1, len1, utf8_byte_offset, explicit);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} contract
|
||||
* @returns {any}
|
||||
*/
|
||||
export function evaluate_current(contract) {
|
||||
const ret = wasm.evaluate_current(contract);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} snapshot
|
||||
* @param {any} contract
|
||||
@@ -75,6 +118,16 @@ export function format_source(source) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} snapshot
|
||||
*/
|
||||
export function set_snapshot(snapshot) {
|
||||
const ret = wasm.set_snapshot(snapshot);
|
||||
if (ret[1]) {
|
||||
throw takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function __wbg_get_imports() {
|
||||
const import0 = {
|
||||
__proto__: null,
|
||||
@@ -199,6 +252,16 @@ function __wbg_get_imports() {
|
||||
const ret = result;
|
||||
return ret;
|
||||
},
|
||||
__wbg_instanceof_Map_a10a2795ef4bfe97: function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof Map;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
},
|
||||
__wbg_instanceof_Uint8Array_4b8da683deb25d72: function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
|
||||
Binary file not shown.
+4
@@ -2,8 +2,12 @@
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const analyze_snapshot: (a: any, b: number, c: number, d: number, e: number) => [number, number, number];
|
||||
export const apply_changes: (a: any) => [number, number, number];
|
||||
export const complete_current: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
|
||||
export const evaluate_current: (a: any) => [number, number, number];
|
||||
export const evaluate_snapshot: (a: any, b: any) => [number, number, number];
|
||||
export const format_source: (a: number, b: number) => [number, number, number, number];
|
||||
export const set_snapshot: (a: any) => [number, number];
|
||||
export const formatSource: (a: number, b: number) => [number, number];
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
|
||||
@@ -1,75 +1,57 @@
|
||||
import type {
|
||||
ConfigDiagnostic,
|
||||
ConfigTreeSnapshot,
|
||||
ToolchainContract,
|
||||
} from "./types.ts";
|
||||
import type {
|
||||
ConfigSourceWorkerRequest,
|
||||
ConfigSourceWorkerResponse,
|
||||
} from "./toolchain.worker.ts";
|
||||
import type { ConfigDiagnostic, ConfigTreeChange, ConfigTreeSnapshot, ToolchainContract } from "./types.ts";
|
||||
import type { ConfigSourceWorkerRequest, ConfigSourceWorkerResponse } from "./toolchain.worker.ts";
|
||||
|
||||
type ConfigSourceWorkerCommand =
|
||||
type Command =
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "set_snapshot" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "apply_changes" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "analyze" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "evaluate" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "complete" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "format" }>, "id">;
|
||||
|
||||
export class ConfigSourceToolchain {
|
||||
#worker: Worker;
|
||||
#nextId = 1;
|
||||
#pending = new Map<
|
||||
number,
|
||||
{ resolve: (value: unknown) => void; reject: (reason: unknown) => void }
|
||||
>();
|
||||
#pending = new Map<number, { resolve: (value: unknown) => void; reject: (reason: unknown) => void }>();
|
||||
|
||||
constructor(
|
||||
worker = new Worker(new URL("./toolchain.worker.ts", import.meta.url), {
|
||||
type: "module",
|
||||
}),
|
||||
) {
|
||||
constructor(worker = new Worker(new URL("./toolchain.worker.ts", import.meta.url), { type: "module" })) {
|
||||
this.#worker = worker;
|
||||
worker.addEventListener(
|
||||
"message",
|
||||
(event: MessageEvent<ConfigSourceWorkerResponse>) => {
|
||||
const pending = this.#pending.get(event.data.id);
|
||||
if (!pending) return;
|
||||
this.#pending.delete(event.data.id);
|
||||
if (event.data.ok) pending.resolve(event.data.result);
|
||||
else pending.reject(event.data.error);
|
||||
},
|
||||
);
|
||||
worker.addEventListener("message", (event: MessageEvent<ConfigSourceWorkerResponse>) => {
|
||||
const pending = this.#pending.get(event.data.id);
|
||||
if (!pending) return;
|
||||
this.#pending.delete(event.data.id);
|
||||
if (event.data.ok) pending.resolve(event.data.result);
|
||||
else pending.reject(event.data.error);
|
||||
});
|
||||
}
|
||||
|
||||
analyze(
|
||||
snapshot: ConfigTreeSnapshot,
|
||||
path: string,
|
||||
source?: string,
|
||||
): Promise<ConfigDiagnostic[]> {
|
||||
return this.#request({ kind: "analyze", snapshot, path, source });
|
||||
setSnapshot(snapshot: ConfigTreeSnapshot): Promise<void> {
|
||||
return this.#request({ kind: "set_snapshot", snapshot });
|
||||
}
|
||||
|
||||
evaluate(snapshot: ConfigTreeSnapshot, contract: ToolchainContract) {
|
||||
return this.#request({ kind: "evaluate", snapshot, contract });
|
||||
applyChanges(changes: ConfigTreeChange[]): Promise<ConfigTreeSnapshot> {
|
||||
return this.#request({ kind: "apply_changes", changes });
|
||||
}
|
||||
analyze(path: string, source?: string): Promise<ConfigDiagnostic[]> {
|
||||
return this.#request({ kind: "analyze", path, source });
|
||||
}
|
||||
evaluate(contract: ToolchainContract) {
|
||||
return this.#request({ kind: "evaluate", contract });
|
||||
}
|
||||
complete(path: string, source: string, utf8ByteOffset: number, explicit = false): Promise<import("@codemirror/autocomplete").CompletionResult | null> {
|
||||
return this.#request({ kind: "complete", path, source, utf8ByteOffset, explicit });
|
||||
}
|
||||
|
||||
format(source: string): Promise<string> {
|
||||
return this.#request({ kind: "format", source });
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.#worker.terminate();
|
||||
for (const pending of this.#pending.values()) {
|
||||
pending.reject(new Error("config source toolchain was closed"));
|
||||
}
|
||||
for (const pending of this.#pending.values()) pending.reject(new Error("config source toolchain was closed"));
|
||||
this.#pending.clear();
|
||||
}
|
||||
|
||||
#request<T>(request: ConfigSourceWorkerCommand): Promise<T> {
|
||||
#request<T>(request: Command): Promise<T> {
|
||||
const id = this.#nextId++;
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
this.#pending.set(id, {
|
||||
resolve: (value) => resolve(value as T),
|
||||
reject,
|
||||
});
|
||||
this.#pending.set(id, { resolve: (value) => resolve(value as T), reject });
|
||||
this.#worker.postMessage({ ...request, id });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
import init, {
|
||||
analyze_snapshot,
|
||||
evaluate_snapshot,
|
||||
apply_changes,
|
||||
complete_current,
|
||||
evaluate_current,
|
||||
format_source,
|
||||
set_snapshot,
|
||||
} from "./generated/config_source_wasm.js";
|
||||
import type {
|
||||
ConfigDiagnostic,
|
||||
ConfigTreeSnapshot,
|
||||
ToolchainContract,
|
||||
} from "./types.ts";
|
||||
import type { ConfigTreeChange } from "./types.ts";
|
||||
|
||||
export type ConfigSourceWorkerRequest =
|
||||
| {
|
||||
id: number;
|
||||
kind: "analyze";
|
||||
snapshot: ConfigTreeSnapshot;
|
||||
path: string;
|
||||
source?: string;
|
||||
}
|
||||
| {
|
||||
id: number;
|
||||
kind: "evaluate";
|
||||
snapshot: ConfigTreeSnapshot;
|
||||
contract: ToolchainContract;
|
||||
}
|
||||
| { id: number; kind: "set_snapshot"; snapshot: unknown }
|
||||
| { id: number; kind: "apply_changes"; changes: ConfigTreeChange[] }
|
||||
| { id: number; kind: "analyze"; path: string; source?: string }
|
||||
| { id: number; kind: "evaluate"; contract: unknown }
|
||||
| { id: number; kind: "complete"; path: string; source: string; utf8ByteOffset: number; explicit: boolean }
|
||||
| { id: number; kind: "format"; source: string };
|
||||
|
||||
export type ConfigSourceWorkerResponse =
|
||||
@@ -30,24 +21,32 @@ export type ConfigSourceWorkerResponse =
|
||||
| { id: number; ok: false; error: unknown };
|
||||
|
||||
const ready = init();
|
||||
let snapshot: unknown = null;
|
||||
|
||||
self.onmessage = async (
|
||||
event: MessageEvent<ConfigSourceWorkerRequest>,
|
||||
): Promise<void> => {
|
||||
self.onmessage = async (event: MessageEvent<ConfigSourceWorkerRequest>): Promise<void> => {
|
||||
const request = event.data;
|
||||
try {
|
||||
await ready;
|
||||
let result: unknown;
|
||||
switch (request.kind) {
|
||||
case "set_snapshot":
|
||||
snapshot = request.snapshot;
|
||||
set_snapshot(request.snapshot);
|
||||
result = null;
|
||||
break;
|
||||
case "apply_changes":
|
||||
snapshot = apply_changes(request.changes);
|
||||
result = snapshot;
|
||||
break;
|
||||
case "analyze":
|
||||
result = analyze_snapshot(
|
||||
request.snapshot,
|
||||
request.path,
|
||||
request.source,
|
||||
) as ConfigDiagnostic[];
|
||||
if (!snapshot) throw new Error("config source snapshot is not initialized");
|
||||
result = analyze_snapshot(snapshot, request.path, request.source);
|
||||
break;
|
||||
case "evaluate":
|
||||
result = evaluate_snapshot(request.snapshot, request.contract);
|
||||
result = evaluate_current(request.contract);
|
||||
break;
|
||||
case "complete":
|
||||
result = complete_current(request.path, request.source, request.utf8ByteOffset, request.explicit);
|
||||
break;
|
||||
case "format":
|
||||
result = format_source(request.source);
|
||||
@@ -55,15 +54,6 @@ self.onmessage = async (
|
||||
}
|
||||
self.postMessage({ id: request.id, ok: true, result });
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
id: request.id,
|
||||
ok: false,
|
||||
error: normalizeError(error),
|
||||
});
|
||||
self.postMessage({ id: request.id, ok: false, error: error instanceof Error ? error.message : error });
|
||||
}
|
||||
};
|
||||
|
||||
function normalizeError(error: unknown): unknown {
|
||||
if (error instanceof Error) return error.message;
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -89,4 +89,5 @@ export interface ConfigCommitRequest {
|
||||
base_digest: string;
|
||||
changes: ConfigTreeChange[];
|
||||
entrypoints: VirtualPath[];
|
||||
toolchain_fingerprint: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { autocompletion, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection } from '@codemirror/view';
|
||||
import { decodal } from 'decodal-codemirror';
|
||||
@@ -9,11 +10,13 @@
|
||||
readonly = false,
|
||||
ariaLabel = 'Decodal source',
|
||||
onChange = (_value: string) => {},
|
||||
onComplete = undefined,
|
||||
}: {
|
||||
value?: string;
|
||||
readonly?: boolean;
|
||||
ariaLabel?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onComplete?: (source: string, utf8ByteOffset: number, explicit: boolean) => Promise<CompletionResult | null>;
|
||||
} = $props();
|
||||
|
||||
let host = $state<HTMLDivElement | null>(null);
|
||||
@@ -39,6 +42,7 @@
|
||||
const initialValue = untrack(() => value);
|
||||
const initialReadonly = untrack(() => readonly);
|
||||
const handleChange = untrack(() => onChange);
|
||||
const handleComplete = untrack(() => onComplete);
|
||||
const editor = new EditorView({
|
||||
parent: host,
|
||||
state: EditorState.create({
|
||||
@@ -48,6 +52,11 @@
|
||||
drawSelection(),
|
||||
highlightActiveLine(),
|
||||
decodal(),
|
||||
...(handleComplete ? [autocompletion({ override: [async (context: CompletionContext) => {
|
||||
const doc = context.state.doc.toString();
|
||||
const utf8ByteOffset = new TextEncoder().encode(doc.slice(0, context.pos)).byteLength;
|
||||
return await handleComplete(doc, utf8ByteOffset, context.explicit);
|
||||
}] })] : []),
|
||||
keymap.of([]),
|
||||
EditorState.readOnly.of(initialReadonly),
|
||||
EditorView.editable.of(!initialReadonly),
|
||||
|
||||
@@ -572,7 +572,8 @@
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.config-source-create input {
|
||||
.config-source-create input,
|
||||
.config-source-actions input {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.45rem;
|
||||
|
||||
@@ -24,12 +24,13 @@ Deno.test("config source API stays workspace-scoped and separates preview from c
|
||||
|
||||
await fetchConfigTree("w/one", fetcher);
|
||||
await fetchConfigEntry("w/one", "profiles/main.dcdl", fetcher);
|
||||
await previewConfigTree("w/one", { changes: [], entrypoints: [] }, fetcher);
|
||||
await previewConfigTree("w/one", { changes: [], entrypoints: [], toolchain_fingerprint: "sha256:toolchain" }, fetcher);
|
||||
await commitConfigTree("w/one", {
|
||||
base_revision: 4,
|
||||
base_digest: "sha256:base",
|
||||
changes: [],
|
||||
entrypoints: [],
|
||||
toolchain_fingerprint: "sha256:toolchain",
|
||||
}, fetcher);
|
||||
|
||||
assertEquals(calls.map((call) => call.url), [
|
||||
@@ -52,7 +53,7 @@ Deno.test("config source API surfaces failed evaluation instead of treating it a
|
||||
)) as typeof fetch;
|
||||
let message = "";
|
||||
try {
|
||||
await previewConfigTree("w", { changes: [], entrypoints: [] }, fetcher);
|
||||
await previewConfigTree("w", { changes: [], entrypoints: [], toolchain_fingerprint: "sha256:toolchain" }, fetcher);
|
||||
} catch (error) {
|
||||
message = String(error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user