config: complete virtual tree editor contract
This commit is contained in:
@@ -1,9 +1,102 @@
|
|||||||
use config_source::{
|
use config_source::{
|
||||||
ConfigTreeSnapshot, EvaluationResult, SnapshotEnvironment, ToolchainContract, VirtualPath,
|
ConfigTreeChange, ConfigTreeSnapshot, EvaluationResult, SnapshotEnvironment, ToolchainContract,
|
||||||
|
VirtualPath,
|
||||||
};
|
};
|
||||||
use serde_wasm_bindgen::{Serializer, from_value};
|
use serde_wasm_bindgen::{Serializer, from_value};
|
||||||
|
use std::cell::RefCell;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static SESSION: RefCell<Option<ConfigTreeSnapshot>> = const { RefCell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn set_snapshot(snapshot: JsValue) -> Result<(), JsValue> {
|
||||||
|
let snapshot: ConfigTreeSnapshot = decode(snapshot)?;
|
||||||
|
SESSION.with(|session| session.replace(Some(snapshot)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn apply_changes(changes: JsValue) -> Result<JsValue, JsValue> {
|
||||||
|
let changes: Vec<ConfigTreeChange> = decode(changes)?;
|
||||||
|
SESSION.with(|session| {
|
||||||
|
let mut session = session.borrow_mut();
|
||||||
|
let snapshot = session
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| JsValue::from_str("config source snapshot is not initialized"))?
|
||||||
|
.apply(&changes)
|
||||||
|
.map_err(js_error)?;
|
||||||
|
*session = Some(snapshot.clone());
|
||||||
|
encode(snapshot)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn evaluate_current(contract: JsValue) -> Result<JsValue, JsValue> {
|
||||||
|
let contract: ToolchainContract = decode(contract)?;
|
||||||
|
SESSION.with(|session| {
|
||||||
|
let session = session.borrow();
|
||||||
|
let snapshot = session
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| JsValue::from_str("config source snapshot is not initialized"))?;
|
||||||
|
encode(
|
||||||
|
SnapshotEnvironment::new(snapshot.clone())
|
||||||
|
.evaluate_contract(&contract)
|
||||||
|
.map_err(|diagnostics| {
|
||||||
|
encode(&diagnostics).unwrap_or_else(|_| JsValue::from_str("evaluation failed"))
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct WasmCompletionResult {
|
||||||
|
from: usize,
|
||||||
|
items: Vec<WasmCompletionItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct WasmCompletionItem {
|
||||||
|
label: String,
|
||||||
|
kind: String,
|
||||||
|
detail: Option<String>,
|
||||||
|
priority: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn complete_current(
|
||||||
|
entrypoint: String,
|
||||||
|
source: String,
|
||||||
|
utf8_byte_offset: usize,
|
||||||
|
explicit: bool,
|
||||||
|
) -> Result<JsValue, JsValue> {
|
||||||
|
let entrypoint = VirtualPath::parse(entrypoint).map_err(js_error)?;
|
||||||
|
SESSION.with(|session| {
|
||||||
|
let session = session.borrow();
|
||||||
|
let snapshot = session
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| JsValue::from_str("config source snapshot is not initialized"))?;
|
||||||
|
let result = SnapshotEnvironment::new(snapshot.clone())
|
||||||
|
.complete(&entrypoint, &source, utf8_byte_offset, explicit)
|
||||||
|
.map_err(|error| JsValue::from_str(&format!("{error:?}")))?
|
||||||
|
.map(|result| WasmCompletionResult {
|
||||||
|
from: result.from,
|
||||||
|
items: result
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| WasmCompletionItem {
|
||||||
|
label: item.label,
|
||||||
|
kind: format!("{:?}", item.kind).to_lowercase(),
|
||||||
|
detail: item.detail,
|
||||||
|
priority: item.priority,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
encode(result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn evaluate_snapshot(snapshot: JsValue, contract: JsValue) -> Result<JsValue, JsValue> {
|
pub fn evaluate_snapshot(snapshot: JsValue, contract: JsValue) -> Result<JsValue, JsValue> {
|
||||||
let snapshot: ConfigTreeSnapshot = decode(snapshot)?;
|
let snapshot: ConfigTreeSnapshot = decode(snapshot)?;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ pub struct ConfigCommitRequest {
|
|||||||
pub base_digest: String,
|
pub base_digest: String,
|
||||||
pub changes: Vec<ConfigTreeChange>,
|
pub changes: Vec<ConfigTreeChange>,
|
||||||
pub entrypoints: Vec<VirtualPath>,
|
pub entrypoints: Vec<VirtualPath>,
|
||||||
|
pub toolchain_fingerprint: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
@@ -65,6 +66,17 @@ impl SqliteWorkspaceStore {
|
|||||||
current.snapshot.revision
|
current.snapshot.revision
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
let expected_contract = ToolchainContract::new(
|
||||||
|
DEFAULT_SCHEMA_VERSION,
|
||||||
|
request.entrypoints.clone(),
|
||||||
|
DEFAULT_IMPORT_POLICY_VERSION,
|
||||||
|
);
|
||||||
|
if expected_contract.fingerprint != request.toolchain_fingerprint {
|
||||||
|
return Err(config_conflict(format!(
|
||||||
|
"toolchain fingerprint mismatch; current fingerprint is {}",
|
||||||
|
expected_contract.fingerprint
|
||||||
|
)));
|
||||||
|
}
|
||||||
evaluate_candidate(current, &request.changes, request.entrypoints.clone())
|
evaluate_candidate(current, &request.changes, request.entrypoints.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,6 +390,12 @@ mod tests {
|
|||||||
content: "{ broken = ; }".into(),
|
content: "{ broken = ; }".into(),
|
||||||
}],
|
}],
|
||||||
entrypoints: vec![path(DEFAULT_CONFIG_ENTRYPOINT)],
|
entrypoints: vec![path(DEFAULT_CONFIG_ENTRYPOINT)],
|
||||||
|
toolchain_fingerprint: ToolchainContract::new(
|
||||||
|
DEFAULT_SCHEMA_VERSION,
|
||||||
|
vec![path(DEFAULT_CONFIG_ENTRYPOINT)],
|
||||||
|
DEFAULT_IMPORT_POLICY_VERSION,
|
||||||
|
)
|
||||||
|
.fingerprint,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
@@ -402,6 +420,12 @@ mod tests {
|
|||||||
content: "{ answer = 42; }".into(),
|
content: "{ answer = 42; }".into(),
|
||||||
}],
|
}],
|
||||||
entrypoints: vec![path(DEFAULT_CONFIG_ENTRYPOINT)],
|
entrypoints: vec![path(DEFAULT_CONFIG_ENTRYPOINT)],
|
||||||
|
toolchain_fingerprint: ToolchainContract::new(
|
||||||
|
DEFAULT_SCHEMA_VERSION,
|
||||||
|
vec![path(DEFAULT_CONFIG_ENTRYPOINT)],
|
||||||
|
DEFAULT_IMPORT_POLICY_VERSION,
|
||||||
|
)
|
||||||
|
.fingerprint,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -426,6 +450,12 @@ mod tests {
|
|||||||
content: "{ answer = 42; }".into(),
|
content: "{ answer = 42; }".into(),
|
||||||
}],
|
}],
|
||||||
entrypoints: vec![path(DEFAULT_CONFIG_ENTRYPOINT)],
|
entrypoints: vec![path(DEFAULT_CONFIG_ENTRYPOINT)],
|
||||||
|
toolchain_fingerprint: ToolchainContract::new(
|
||||||
|
DEFAULT_SCHEMA_VERSION,
|
||||||
|
vec![path(DEFAULT_CONFIG_ENTRYPOINT)],
|
||||||
|
DEFAULT_IMPORT_POLICY_VERSION,
|
||||||
|
)
|
||||||
|
.fingerprint,
|
||||||
};
|
};
|
||||||
let candidate = store
|
let candidate = store
|
||||||
.evaluate_workspace_config_candidate("w-config", &request)
|
.evaluate_workspace_config_candidate("w-config", &request)
|
||||||
@@ -439,6 +469,31 @@ mod tests {
|
|||||||
assert!(matches!(error, Error::WorkspaceConfigConflict(_)));
|
assert!(matches!(error, Error::WorkspaceConfigConflict(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn commit_rejects_mismatched_toolchain_fingerprint() {
|
||||||
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
|
store.upsert_workspace(&workspace()).await.unwrap();
|
||||||
|
let empty = ConfigTreeSnapshot::empty();
|
||||||
|
let error = store
|
||||||
|
.evaluate_and_commit_workspace_config(
|
||||||
|
"w-config",
|
||||||
|
&ConfigCommitRequest {
|
||||||
|
base_revision: 0,
|
||||||
|
base_digest: empty.digest,
|
||||||
|
changes: vec![ConfigTreeChange::Create {
|
||||||
|
path: path(DEFAULT_CONFIG_ENTRYPOINT),
|
||||||
|
content_type: ConfigContentType::Decodal,
|
||||||
|
content: "{ answer = 42; }".into(),
|
||||||
|
}],
|
||||||
|
entrypoints: vec![path(DEFAULT_CONFIG_ENTRYPOINT)],
|
||||||
|
toolchain_fingerprint: "sha256:stale-toolchain".into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(error, Error::WorkspaceConfigConflict(_)));
|
||||||
|
assert!(store.load_workspace_config("w-config").unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn migration_creates_config_authority_without_changing_applied_migrations() {
|
fn migration_creates_config_authority_without_changing_applied_migrations() {
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@sveltejs/adapter-static": "npm:@sveltejs/adapter-static@3.0.9",
|
"@sveltejs/adapter-static": "npm:@sveltejs/adapter-static@3.0.9",
|
||||||
"@sveltejs/kit": "npm:@sveltejs/kit@2.49.4",
|
"@sveltejs/kit": "npm:@sveltejs/kit@2.49.4",
|
||||||
"@sveltejs/vite-plugin-svelte": "npm:@sveltejs/vite-plugin-svelte@6.2.1",
|
"@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/state": "npm:@codemirror/state@6.7.1",
|
||||||
"@codemirror/view": "npm:@codemirror/view@6.43.8",
|
"@codemirror/view": "npm:@codemirror/view@6.43.8",
|
||||||
"decodal-codemirror": "npm:decodal-codemirror@0.1.6",
|
"decodal-codemirror": "npm:decodal-codemirror@0.1.6",
|
||||||
|
|||||||
Generated
+12
@@ -3,6 +3,7 @@
|
|||||||
"specifiers": {
|
"specifiers": {
|
||||||
"jsr:@std/assert@*": "1.0.19",
|
"jsr:@std/assert@*": "1.0.19",
|
||||||
"jsr:@std/internal@^1.0.12": "1.0.14",
|
"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/state@6.7.1": "6.7.1",
|
||||||
"npm:@codemirror/view@6.43.8": "6.43.8",
|
"npm:@codemirror/view@6.43.8": "6.43.8",
|
||||||
"npm:@fontsource/ibm-plex-mono@5.3.0": "5.3.0",
|
"npm:@fontsource/ibm-plex-mono@5.3.0": "5.3.0",
|
||||||
@@ -34,9 +35,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
|
"@codemirror/autocomplete@6.20.0": {
|
||||||
|
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
|
||||||
|
"dependencies": [
|
||||||
|
"@codemirror/language",
|
||||||
|
"@codemirror/state",
|
||||||
|
"@codemirror/view",
|
||||||
|
"@lezer/common"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@codemirror/language@6.12.4": {
|
"@codemirror/language@6.12.4": {
|
||||||
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"@codemirror/state",
|
||||||
"@codemirror/view",
|
"@codemirror/view",
|
||||||
"@lezer/common",
|
"@lezer/common",
|
||||||
"@lezer/highlight",
|
"@lezer/highlight",
|
||||||
@@ -988,6 +999,7 @@
|
|||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"npm:@codemirror/autocomplete@6.20.0",
|
||||||
"npm:@codemirror/state@6.7.1",
|
"npm:@codemirror/state@6.7.1",
|
||||||
"npm:@codemirror/view@6.43.8",
|
"npm:@codemirror/view@6.43.8",
|
||||||
"npm:@sveltejs/adapter-static@3.0.9",
|
"npm:@sveltejs/adapter-static@3.0.9",
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
let diagnostics = $state<ConfigDiagnostic[]>([]);
|
let diagnostics = $state<ConfigDiagnostic[]>([]);
|
||||||
let status = $state("Loading source tree…");
|
let status = $state("Loading source tree…");
|
||||||
let busy = $state(false);
|
let busy = $state(false);
|
||||||
|
let draftChanges = $state<ConfigTreeChange[]>([]);
|
||||||
|
let baseRevision = $state(0);
|
||||||
|
let baseDigest = $state("");
|
||||||
|
let renamePath = $state("");
|
||||||
let toolchain: ConfigSourceToolchain | null = null;
|
let toolchain: ConfigSourceToolchain | null = null;
|
||||||
|
|
||||||
const paths = $derived(
|
const paths = $derived(
|
||||||
@@ -29,7 +33,7 @@
|
|||||||
const selected = $derived(
|
const selected = $derived(
|
||||||
treeState && selectedPath ? treeState.snapshot.entries[selectedPath] : undefined,
|
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(() => {
|
onMount(() => {
|
||||||
toolchain = new ConfigSourceToolchain();
|
toolchain = new ConfigSourceToolchain();
|
||||||
@@ -44,6 +48,11 @@
|
|||||||
selectedPath = Object.keys(treeState.snapshot.entries).toSorted()[0] ?? "";
|
selectedPath = Object.keys(treeState.snapshot.entries).toSorted()[0] ?? "";
|
||||||
}
|
}
|
||||||
source = selectedPath ? treeState.snapshot.entries[selectedPath].content : "";
|
source = selectedPath ? treeState.snapshot.entries[selectedPath].content : "";
|
||||||
|
baseRevision = treeState.snapshot.revision;
|
||||||
|
baseDigest = treeState.snapshot.digest;
|
||||||
|
await toolchain?.setSnapshot(treeState.snapshot);
|
||||||
|
draftChanges = [];
|
||||||
|
renamePath = selectedPath;
|
||||||
diagnostics = [];
|
diagnostics = [];
|
||||||
status = treeState.snapshot.revision === 0
|
status = treeState.snapshot.revision === 0
|
||||||
? "No committed sources yet. Create workspace.dcdl to begin."
|
? "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;
|
selectedPath = path;
|
||||||
source = treeState?.snapshot.entries[path]?.content ?? "";
|
source = treeState?.snapshot.entries[path]?.content ?? "";
|
||||||
|
renamePath = path;
|
||||||
diagnostics = [];
|
diagnostics = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +104,9 @@
|
|||||||
|
|
||||||
function entrypoints(): string[] {
|
function entrypoints(): string[] {
|
||||||
if (!treeState) return [];
|
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") {
|
if (treeState.snapshot.entries["workspace.dcdl"] || selectedPath === "workspace.dcdl") {
|
||||||
return ["workspace.dcdl"];
|
return ["workspace.dcdl"];
|
||||||
}
|
}
|
||||||
@@ -90,7 +115,7 @@
|
|||||||
|
|
||||||
async function analyze() {
|
async function analyze() {
|
||||||
if (!toolchain || !treeState || !selectedPath) return;
|
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).`;
|
status = diagnostics.length === 0 ? "No diagnostics." : `${diagnostics.length} diagnostic(s).`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,16 +131,17 @@
|
|||||||
|
|
||||||
async function preview() {
|
async function preview() {
|
||||||
if (!treeState) return;
|
if (!treeState) return;
|
||||||
const change = currentChange();
|
await stageCurrent();
|
||||||
if (!change) {
|
if (draftChanges.length === 0) {
|
||||||
status = "No draft changes to preview.";
|
status = "No draft changes to preview.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
busy = true;
|
busy = true;
|
||||||
try {
|
try {
|
||||||
const candidate = await previewConfigTree(workspaceId, {
|
const candidate = await previewConfigTree(workspaceId, {
|
||||||
changes: [change],
|
changes: draftChanges,
|
||||||
entrypoints: entrypoints(),
|
entrypoints: entrypoints(),
|
||||||
|
toolchain_fingerprint: treeState.contract.fingerprint,
|
||||||
});
|
});
|
||||||
diagnostics = [];
|
diagnostics = [];
|
||||||
status = `Preview valid · projection ${candidate.evaluation.projection_digest.slice(0, 20)}…`;
|
status = `Preview valid · projection ${candidate.evaluation.projection_digest.slice(0, 20)}…`;
|
||||||
@@ -128,19 +154,24 @@
|
|||||||
|
|
||||||
async function commit() {
|
async function commit() {
|
||||||
if (!treeState) return;
|
if (!treeState) return;
|
||||||
const change = currentChange();
|
await stageCurrent();
|
||||||
if (!change) {
|
if (draftChanges.length === 0) {
|
||||||
status = "No draft changes to commit.";
|
status = "No draft changes to commit.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
busy = true;
|
busy = true;
|
||||||
try {
|
try {
|
||||||
treeState = await commitConfigTree(workspaceId, {
|
treeState = await commitConfigTree(workspaceId, {
|
||||||
base_revision: treeState.snapshot.revision,
|
base_revision: baseRevision,
|
||||||
base_digest: treeState.snapshot.digest,
|
base_digest: baseDigest,
|
||||||
changes: [change],
|
changes: draftChanges,
|
||||||
entrypoints: entrypoints(),
|
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 ?? "";
|
source = treeState.snapshot.entries[selectedPath]?.content ?? "";
|
||||||
diagnostics = [];
|
diagnostics = [];
|
||||||
status = `Committed revision ${treeState.snapshot.revision}.`;
|
status = `Committed revision ${treeState.snapshot.revision}.`;
|
||||||
@@ -161,29 +192,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteEntry() {
|
async function deleteEntry() {
|
||||||
if (!treeState || !selected) return;
|
if (!treeState || !selected || !toolchain) return;
|
||||||
busy = true;
|
const change: ConfigTreeChange = {
|
||||||
try {
|
kind: "delete",
|
||||||
const remainingEntrypoints = treeState.contract.entrypoints.filter((path) => path !== selectedPath);
|
path: selectedPath,
|
||||||
treeState = await commitConfigTree(workspaceId, {
|
expected_digest: selected.content_digest,
|
||||||
base_revision: treeState.snapshot.revision,
|
};
|
||||||
base_digest: treeState.snapshot.digest,
|
draftChanges = [...draftChanges.filter((item) => !changeTouches(item, selectedPath)), change];
|
||||||
changes: [{
|
const candidate = await toolchain.applyChanges([change]);
|
||||||
kind: "delete",
|
treeState = { ...treeState, snapshot: candidate };
|
||||||
path: selectedPath,
|
selectedPath = Object.keys(candidate.entries).toSorted()[0] ?? "";
|
||||||
expected_digest: selected.content_digest,
|
source = selectedPath ? candidate.entries[selectedPath].content : "";
|
||||||
}],
|
renamePath = selectedPath;
|
||||||
entrypoints: remainingEntrypoints,
|
status = "Delete staged. Preview and Commit to persist the candidate tree.";
|
||||||
});
|
}
|
||||||
selectedPath = Object.keys(treeState.snapshot.entries).toSorted()[0] ?? "";
|
|
||||||
source = selectedPath ? treeState.snapshot.entries[selectedPath].content : "";
|
async function renameEntry() {
|
||||||
diagnostics = [];
|
if (!treeState || !selected || !toolchain) return;
|
||||||
status = `Committed revision ${treeState.snapshot.revision}.`;
|
const to = renamePath.trim();
|
||||||
} catch (error) {
|
if (!to || to === selectedPath) return;
|
||||||
status = String(error);
|
await stageCurrent();
|
||||||
} finally {
|
const change: ConfigTreeChange = {
|
||||||
busy = false;
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -216,6 +256,8 @@
|
|||||||
<strong>{selectedPath || "Select or create a source"}</strong>
|
<strong>{selectedPath || "Select or create a source"}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-source-actions">
|
<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={format} disabled={!selectedPath || busy}>Format</button>
|
||||||
<button type="button" onclick={analyze} disabled={!selectedPath || busy}>Analyze</button>
|
<button type="button" onclick={analyze} disabled={!selectedPath || busy}>Analyze</button>
|
||||||
<button type="button" onclick={preview} disabled={!dirty || busy}>Preview</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>
|
<button class="danger" type="button" onclick={deleteEntry} disabled={!selected || busy}>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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>
|
<p class="config-source-status" aria-live="polite">{status}</p>
|
||||||
{#if diagnostics.length > 0}
|
{#if diagnostics.length > 0}
|
||||||
<ol class="config-source-diagnostics">
|
<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 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 evaluate_snapshot(snapshot: any, contract: any): any;
|
||||||
|
|
||||||
export function formatSource(source: string): string;
|
export function formatSource(source: string): string;
|
||||||
|
|
||||||
export function format_source(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 type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
export interface InitOutput {
|
export interface InitOutput {
|
||||||
readonly memory: WebAssembly.Memory;
|
readonly memory: WebAssembly.Memory;
|
||||||
readonly analyze_snapshot: (a: any, b: number, c: number, d: number, e: number) => [number, number, number];
|
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 evaluate_snapshot: (a: any, b: any) => [number, number, number];
|
||||||
readonly format_source: (a: number, b: number) => [number, 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 formatSource: (a: number, b: number) => [number, number];
|
||||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: 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]);
|
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} snapshot
|
||||||
* @param {any} contract
|
* @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() {
|
function __wbg_get_imports() {
|
||||||
const import0 = {
|
const import0 = {
|
||||||
__proto__: null,
|
__proto__: null,
|
||||||
@@ -199,6 +252,16 @@ function __wbg_get_imports() {
|
|||||||
const ret = result;
|
const ret = result;
|
||||||
return ret;
|
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) {
|
__wbg_instanceof_Uint8Array_4b8da683deb25d72: function(arg0) {
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Binary file not shown.
+4
@@ -2,8 +2,12 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export const memory: WebAssembly.Memory;
|
export const memory: WebAssembly.Memory;
|
||||||
export const analyze_snapshot: (a: any, b: number, c: number, d: number, e: number) => [number, number, number];
|
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 evaluate_snapshot: (a: any, b: any) => [number, number, number];
|
||||||
export const format_source: (a: number, b: number) => [number, 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 formatSource: (a: number, b: number) => [number, number];
|
||||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
|||||||
@@ -1,75 +1,57 @@
|
|||||||
import type {
|
import type { ConfigDiagnostic, ConfigTreeChange, ConfigTreeSnapshot, ToolchainContract } from "./types.ts";
|
||||||
ConfigDiagnostic,
|
import type { ConfigSourceWorkerRequest, ConfigSourceWorkerResponse } from "./toolchain.worker.ts";
|
||||||
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: "analyze" }>, "id">
|
||||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "evaluate" }>, "id">
|
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "evaluate" }>, "id">
|
||||||
|
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "complete" }>, "id">
|
||||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "format" }>, "id">;
|
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "format" }>, "id">;
|
||||||
|
|
||||||
export class ConfigSourceToolchain {
|
export class ConfigSourceToolchain {
|
||||||
#worker: Worker;
|
#worker: Worker;
|
||||||
#nextId = 1;
|
#nextId = 1;
|
||||||
#pending = new Map<
|
#pending = new Map<number, { resolve: (value: unknown) => void; reject: (reason: unknown) => void }>();
|
||||||
number,
|
|
||||||
{ resolve: (value: unknown) => void; reject: (reason: unknown) => void }
|
|
||||||
>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(worker = new Worker(new URL("./toolchain.worker.ts", import.meta.url), { type: "module" })) {
|
||||||
worker = new Worker(new URL("./toolchain.worker.ts", import.meta.url), {
|
|
||||||
type: "module",
|
|
||||||
}),
|
|
||||||
) {
|
|
||||||
this.#worker = worker;
|
this.#worker = worker;
|
||||||
worker.addEventListener(
|
worker.addEventListener("message", (event: MessageEvent<ConfigSourceWorkerResponse>) => {
|
||||||
"message",
|
const pending = this.#pending.get(event.data.id);
|
||||||
(event: MessageEvent<ConfigSourceWorkerResponse>) => {
|
if (!pending) return;
|
||||||
const pending = this.#pending.get(event.data.id);
|
this.#pending.delete(event.data.id);
|
||||||
if (!pending) return;
|
if (event.data.ok) pending.resolve(event.data.result);
|
||||||
this.#pending.delete(event.data.id);
|
else pending.reject(event.data.error);
|
||||||
if (event.data.ok) pending.resolve(event.data.result);
|
});
|
||||||
else pending.reject(event.data.error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
analyze(
|
setSnapshot(snapshot: ConfigTreeSnapshot): Promise<void> {
|
||||||
snapshot: ConfigTreeSnapshot,
|
return this.#request({ kind: "set_snapshot", snapshot });
|
||||||
path: string,
|
|
||||||
source?: string,
|
|
||||||
): Promise<ConfigDiagnostic[]> {
|
|
||||||
return this.#request({ kind: "analyze", snapshot, path, source });
|
|
||||||
}
|
}
|
||||||
|
applyChanges(changes: ConfigTreeChange[]): Promise<ConfigTreeSnapshot> {
|
||||||
evaluate(snapshot: ConfigTreeSnapshot, contract: ToolchainContract) {
|
return this.#request({ kind: "apply_changes", changes });
|
||||||
return this.#request({ kind: "evaluate", snapshot, contract });
|
}
|
||||||
|
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> {
|
format(source: string): Promise<string> {
|
||||||
return this.#request({ kind: "format", source });
|
return this.#request({ kind: "format", source });
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
this.#worker.terminate();
|
this.#worker.terminate();
|
||||||
for (const pending of this.#pending.values()) {
|
for (const pending of this.#pending.values()) pending.reject(new Error("config source toolchain was closed"));
|
||||||
pending.reject(new Error("config source toolchain was closed"));
|
|
||||||
}
|
|
||||||
this.#pending.clear();
|
this.#pending.clear();
|
||||||
}
|
}
|
||||||
|
#request<T>(request: Command): Promise<T> {
|
||||||
#request<T>(request: ConfigSourceWorkerCommand): Promise<T> {
|
|
||||||
const id = this.#nextId++;
|
const id = this.#nextId++;
|
||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
this.#pending.set(id, {
|
this.#pending.set(id, { resolve: (value) => resolve(value as T), reject });
|
||||||
resolve: (value) => resolve(value as T),
|
|
||||||
reject,
|
|
||||||
});
|
|
||||||
this.#worker.postMessage({ ...request, id });
|
this.#worker.postMessage({ ...request, id });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,19 @@
|
|||||||
import init, {
|
import init, {
|
||||||
analyze_snapshot,
|
analyze_snapshot,
|
||||||
evaluate_snapshot,
|
apply_changes,
|
||||||
|
complete_current,
|
||||||
|
evaluate_current,
|
||||||
format_source,
|
format_source,
|
||||||
|
set_snapshot,
|
||||||
} from "./generated/config_source_wasm.js";
|
} from "./generated/config_source_wasm.js";
|
||||||
import type {
|
import type { ConfigTreeChange } from "./types.ts";
|
||||||
ConfigDiagnostic,
|
|
||||||
ConfigTreeSnapshot,
|
|
||||||
ToolchainContract,
|
|
||||||
} from "./types.ts";
|
|
||||||
|
|
||||||
export type ConfigSourceWorkerRequest =
|
export type ConfigSourceWorkerRequest =
|
||||||
| {
|
| { id: number; kind: "set_snapshot"; snapshot: unknown }
|
||||||
id: number;
|
| { id: number; kind: "apply_changes"; changes: ConfigTreeChange[] }
|
||||||
kind: "analyze";
|
| { id: number; kind: "analyze"; path: string; source?: string }
|
||||||
snapshot: ConfigTreeSnapshot;
|
| { id: number; kind: "evaluate"; contract: unknown }
|
||||||
path: string;
|
| { id: number; kind: "complete"; path: string; source: string; utf8ByteOffset: number; explicit: boolean }
|
||||||
source?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: number;
|
|
||||||
kind: "evaluate";
|
|
||||||
snapshot: ConfigTreeSnapshot;
|
|
||||||
contract: ToolchainContract;
|
|
||||||
}
|
|
||||||
| { id: number; kind: "format"; source: string };
|
| { id: number; kind: "format"; source: string };
|
||||||
|
|
||||||
export type ConfigSourceWorkerResponse =
|
export type ConfigSourceWorkerResponse =
|
||||||
@@ -30,24 +21,32 @@ export type ConfigSourceWorkerResponse =
|
|||||||
| { id: number; ok: false; error: unknown };
|
| { id: number; ok: false; error: unknown };
|
||||||
|
|
||||||
const ready = init();
|
const ready = init();
|
||||||
|
let snapshot: unknown = null;
|
||||||
|
|
||||||
self.onmessage = async (
|
self.onmessage = async (event: MessageEvent<ConfigSourceWorkerRequest>): Promise<void> => {
|
||||||
event: MessageEvent<ConfigSourceWorkerRequest>,
|
|
||||||
): Promise<void> => {
|
|
||||||
const request = event.data;
|
const request = event.data;
|
||||||
try {
|
try {
|
||||||
await ready;
|
await ready;
|
||||||
let result: unknown;
|
let result: unknown;
|
||||||
switch (request.kind) {
|
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":
|
case "analyze":
|
||||||
result = analyze_snapshot(
|
if (!snapshot) throw new Error("config source snapshot is not initialized");
|
||||||
request.snapshot,
|
result = analyze_snapshot(snapshot, request.path, request.source);
|
||||||
request.path,
|
|
||||||
request.source,
|
|
||||||
) as ConfigDiagnostic[];
|
|
||||||
break;
|
break;
|
||||||
case "evaluate":
|
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;
|
break;
|
||||||
case "format":
|
case "format":
|
||||||
result = format_source(request.source);
|
result = format_source(request.source);
|
||||||
@@ -55,15 +54,6 @@ self.onmessage = async (
|
|||||||
}
|
}
|
||||||
self.postMessage({ id: request.id, ok: true, result });
|
self.postMessage({ id: request.id, ok: true, result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({ id: request.id, ok: false, error: error instanceof Error ? error.message : error });
|
||||||
id: request.id,
|
|
||||||
ok: false,
|
|
||||||
error: normalizeError(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;
|
base_digest: string;
|
||||||
changes: ConfigTreeChange[];
|
changes: ConfigTreeChange[];
|
||||||
entrypoints: VirtualPath[];
|
entrypoints: VirtualPath[];
|
||||||
|
toolchain_fingerprint: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
|
import { autocompletion, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete';
|
||||||
import { EditorState } from '@codemirror/state';
|
import { EditorState } from '@codemirror/state';
|
||||||
import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection } from '@codemirror/view';
|
import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection } from '@codemirror/view';
|
||||||
import { decodal } from 'decodal-codemirror';
|
import { decodal } from 'decodal-codemirror';
|
||||||
@@ -9,11 +10,13 @@
|
|||||||
readonly = false,
|
readonly = false,
|
||||||
ariaLabel = 'Decodal source',
|
ariaLabel = 'Decodal source',
|
||||||
onChange = (_value: string) => {},
|
onChange = (_value: string) => {},
|
||||||
|
onComplete = undefined,
|
||||||
}: {
|
}: {
|
||||||
value?: string;
|
value?: string;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
|
onComplete?: (source: string, utf8ByteOffset: number, explicit: boolean) => Promise<CompletionResult | null>;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let host = $state<HTMLDivElement | null>(null);
|
let host = $state<HTMLDivElement | null>(null);
|
||||||
@@ -39,6 +42,7 @@
|
|||||||
const initialValue = untrack(() => value);
|
const initialValue = untrack(() => value);
|
||||||
const initialReadonly = untrack(() => readonly);
|
const initialReadonly = untrack(() => readonly);
|
||||||
const handleChange = untrack(() => onChange);
|
const handleChange = untrack(() => onChange);
|
||||||
|
const handleComplete = untrack(() => onComplete);
|
||||||
const editor = new EditorView({
|
const editor = new EditorView({
|
||||||
parent: host,
|
parent: host,
|
||||||
state: EditorState.create({
|
state: EditorState.create({
|
||||||
@@ -48,6 +52,11 @@
|
|||||||
drawSelection(),
|
drawSelection(),
|
||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
decodal(),
|
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([]),
|
keymap.of([]),
|
||||||
EditorState.readOnly.of(initialReadonly),
|
EditorState.readOnly.of(initialReadonly),
|
||||||
EditorView.editable.of(!initialReadonly),
|
EditorView.editable.of(!initialReadonly),
|
||||||
|
|||||||
@@ -572,7 +572,8 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
.config-source-create input {
|
.config-source-create input,
|
||||||
|
.config-source-actions input {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 0.45rem;
|
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 fetchConfigTree("w/one", fetcher);
|
||||||
await fetchConfigEntry("w/one", "profiles/main.dcdl", 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", {
|
await commitConfigTree("w/one", {
|
||||||
base_revision: 4,
|
base_revision: 4,
|
||||||
base_digest: "sha256:base",
|
base_digest: "sha256:base",
|
||||||
changes: [],
|
changes: [],
|
||||||
entrypoints: [],
|
entrypoints: [],
|
||||||
|
toolchain_fingerprint: "sha256:toolchain",
|
||||||
}, fetcher);
|
}, fetcher);
|
||||||
|
|
||||||
assertEquals(calls.map((call) => call.url), [
|
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;
|
)) as typeof fetch;
|
||||||
let message = "";
|
let message = "";
|
||||||
try {
|
try {
|
||||||
await previewConfigTree("w", { changes: [], entrypoints: [] }, fetcher);
|
await previewConfigTree("w", { changes: [], entrypoints: [], toolchain_fingerprint: "sha256:toolchain" }, fetcher);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message = String(error);
|
message = String(error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user