diff --git a/Cargo.lock b/Cargo.lock index cdc5792b..de317867 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,6 +601,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "thiserror 2.0.18", + "ts-rs", ] [[package]] diff --git a/crates/config-source-wasm/src/lib.rs b/crates/config-source-wasm/src/lib.rs index 21d64ac7..5b992f64 100644 --- a/crates/config-source-wasm/src/lib.rs +++ b/crates/config-source-wasm/src/lib.rs @@ -32,6 +32,13 @@ pub fn apply_changes(changes: JsValue) -> Result { }) } +#[wasm_bindgen] +pub fn changes_between(base: JsValue, candidate: JsValue) -> Result { + let base: ConfigTreeSnapshot = decode(base)?; + let candidate: ConfigTreeSnapshot = decode(candidate)?; + encode(base.changes_to(&candidate)) +} + #[wasm_bindgen] pub fn evaluate_current(contract: JsValue) -> Result { let contract: ToolchainContract = decode(contract)?; @@ -68,7 +75,7 @@ struct WasmCompletionItem { pub fn complete_current( entrypoint: String, source: String, - utf8_byte_offset: usize, + utf16_offset: usize, explicit: bool, ) -> Result { let entrypoint = VirtualPath::parse(entrypoint).map_err(js_error)?; @@ -77,6 +84,7 @@ pub fn complete_current( let snapshot = session .as_ref() .ok_or_else(|| JsValue::from_str("config source snapshot is not initialized"))?; + let utf8_byte_offset = utf16_to_utf8_offset(&source, utf16_offset)?; let result = SnapshotEnvironment::new(snapshot.clone()) .complete(&entrypoint, &source, utf8_byte_offset, explicit) .map_err(|error| JsValue::from_str(&format!("{error:?}")))? @@ -128,6 +136,24 @@ pub fn format_source(source: String) -> Result { .map_err(|error| JsValue::from_str(&error)) } +fn utf16_to_utf8_offset(source: &str, utf16_offset: usize) -> Result { + let mut units = 0usize; + for (byte_offset, character) in source.char_indices() { + if units == utf16_offset { + return Ok(byte_offset); + } + units += character.len_utf16(); + if units > utf16_offset { + return Err(JsValue::from_str("UTF-16 offset splits a surrogate pair")); + } + } + if units == utf16_offset { + Ok(source.len()) + } else { + Err(JsValue::from_str("UTF-16 offset is outside the source")) + } +} + fn decode(value: JsValue) -> Result { from_value(value).map_err(|error| JsValue::from_str(&error.to_string())) } diff --git a/crates/config-source/Cargo.toml b/crates/config-source/Cargo.toml index 9deb7fa9..d86dc5d5 100644 --- a/crates/config-source/Cargo.toml +++ b/crates/config-source/Cargo.toml @@ -13,6 +13,7 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sha2.workspace = true thiserror.workspace = true +ts-rs = "12.0.1" [dev-dependencies] pretty_assertions = "1" diff --git a/crates/config-source/src/lib.rs b/crates/config-source/src/lib.rs index f1806799..49c0a9f5 100644 --- a/crates/config-source/src/lib.rs +++ b/crates/config-source/src/lib.rs @@ -19,7 +19,7 @@ pub const MAX_ENTRY_BYTES: usize = 256 * 1024; pub const MAX_TOTAL_BYTES: usize = 4 * 1024 * 1024; pub const MAX_PATH_BYTES: usize = 512; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, ts_rs::TS)] #[serde(transparent)] pub struct VirtualPath(String); @@ -71,7 +71,7 @@ impl fmt::Display for VirtualPath { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)] #[serde(rename_all = "snake_case")] pub enum ConfigContentType { Decodal, @@ -87,7 +87,7 @@ impl ConfigContentType { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)] pub struct ConfigEntry { pub path: VirtualPath, pub content_type: ConfigContentType, @@ -114,8 +114,9 @@ impl ConfigEntry { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)] pub struct ConfigTreeSnapshot { + #[ts(type = "number")] pub revision: u64, pub digest: String, pub entries: BTreeMap, @@ -174,6 +175,38 @@ impl ConfigTreeSnapshot { self.entries.get(path) } + pub fn changes_to(&self, candidate: &Self) -> Vec { + let mut changes = Vec::new(); + for (path, base_entry) in &self.entries { + match candidate.entries.get(path) { + None => changes.push(ConfigTreeChange::Delete { + path: path.clone(), + expected_digest: base_entry.content_digest.clone(), + }), + Some(candidate_entry) + if candidate_entry.content_digest != base_entry.content_digest => + { + changes.push(ConfigTreeChange::Update { + path: path.clone(), + expected_digest: base_entry.content_digest.clone(), + content: candidate_entry.content.clone(), + }); + } + Some(_) => {} + } + } + for (path, entry) in &candidate.entries { + if !self.entries.contains_key(path) { + changes.push(ConfigTreeChange::Create { + path: path.clone(), + content_type: entry.content_type, + content: entry.content.clone(), + }); + } + } + changes + } + pub fn apply(&self, changes: &[ConfigTreeChange]) -> Result { if changes.len() > MAX_CHANGE_COUNT { return Err(ConfigTreeError::LimitExceeded("change count")); @@ -253,7 +286,7 @@ impl ConfigTreeSnapshot { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum ConfigTreeChange { Create { @@ -288,7 +321,7 @@ impl ConfigTreeChange { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)] pub struct ToolchainContract { pub contract_version: u32, pub decodal_version: String, @@ -329,21 +362,22 @@ impl ToolchainContract { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)] pub struct ConfigSpan { pub start_byte: u32, pub end_byte: u32, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)] pub struct ConfigDiagnosticLabel { pub span: ConfigSpan, pub message: String, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)] pub struct ConfigDiagnostic { pub path: VirtualPath, + #[ts(type = "number")] pub revision: u64, pub tree_digest: String, pub kind: String, @@ -353,14 +387,16 @@ pub struct ConfigDiagnostic { pub notes: Vec, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)] +#[ts(export)] pub struct EvaluatedProjection { pub entrypoint: VirtualPath, + #[ts(type = "unknown")] pub data_json: serde_json::Value, pub projection_digest: String, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)] pub struct EvaluationResult { pub projections: Vec, pub projection_digest: String, @@ -779,6 +815,30 @@ pub enum ConfigTreeError { mod tests { use super::*; use pretty_assertions::assert_eq; + use ts_rs::TS; + + #[test] + fn exports_typescript_contract() { + let output = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../web/workspace/src/lib/workspace/config-source/generated/types"); + std::fs::create_dir_all(&output).unwrap(); + macro_rules! export { + ($type:ty) => { + <$type>::export_all(&ts_rs::Config::default().with_out_dir(&output)).unwrap(); + }; + } + export!(VirtualPath); + export!(ConfigContentType); + export!(ConfigEntry); + export!(ConfigTreeSnapshot); + export!(ConfigTreeChange); + export!(ToolchainContract); + export!(ConfigSpan); + export!(ConfigDiagnosticLabel); + export!(ConfigDiagnostic); + export!(EvaluatedProjection); + export!(EvaluationResult); + } fn path(value: &str) -> VirtualPath { VirtualPath::parse(value).unwrap() diff --git a/crates/workspace-server/src/config_source.rs b/crates/workspace-server/src/config_source.rs index e9f72a25..2fea1f43 100644 --- a/crates/workspace-server/src/config_source.rs +++ b/crates/workspace-server/src/config_source.rs @@ -11,15 +11,18 @@ use crate::{Error, Result, SqliteWorkspaceStore}; pub const DEFAULT_CONFIG_ENTRYPOINT: &str = "workspace.dcdl"; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)] +#[ts(export)] pub struct WorkspaceConfigState { pub snapshot: ConfigTreeSnapshot, pub contract: ToolchainContract, pub projection_digest: String, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)] +#[ts(export)] pub struct EvaluatedConfigCandidate { + #[ts(type = "number")] pub base_revision: u64, pub base_digest: String, pub snapshot: ConfigTreeSnapshot, @@ -27,8 +30,10 @@ pub struct EvaluatedConfigCandidate { pub evaluation: EvaluationResult, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)] +#[ts(export)] pub struct ConfigCommitRequest { + #[ts(type = "number")] pub base_revision: u64, pub base_digest: String, pub changes: Vec, @@ -36,7 +41,8 @@ pub struct ConfigCommitRequest { pub toolchain_fingerprint: String, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)] +#[ts(export)] pub struct ConfigPreviewRequest { pub changes: Vec, pub entrypoints: Vec, @@ -494,6 +500,18 @@ mod tests { assert!(store.load_workspace_config("w-config").unwrap().is_none()); } + #[test] + fn exports_typescript_transport_contract() { + use ts_rs::TS; + let output = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../web/workspace/src/lib/workspace/config-source/generated/types"); + let config = ts_rs::Config::default().with_out_dir(&output); + WorkspaceConfigState::export_all(&config).unwrap(); + EvaluatedConfigCandidate::export_all(&config).unwrap(); + ConfigCommitRequest::export_all(&config).unwrap(); + ConfigPreviewRequest::export_all(&config).unwrap(); + } + #[test] fn migration_creates_config_authority_without_changing_applied_migrations() { let store = SqliteWorkspaceStore::in_memory().unwrap(); diff --git a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte index ffeae315..ae53e273 100644 --- a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte +++ b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte @@ -25,6 +25,8 @@ let baseRevision = $state(0); let baseDigest = $state(""); let renamePath = $state(""); + let baseSnapshot = $state(null); + let preflightDigest = $state(""); let toolchain: ConfigSourceToolchain | null = null; const paths = $derived( @@ -34,6 +36,7 @@ treeState && selectedPath ? treeState.snapshot.entries[selectedPath] : undefined, ); const dirty = $derived(draftChanges.length > 0 || (selected ? source !== selected.content : source.length > 0)); + const commitReady = $derived(dirty && preflightDigest === treeState?.snapshot.digest); onMount(() => { toolchain = new ConfigSourceToolchain(); @@ -48,6 +51,7 @@ selectedPath = Object.keys(treeState.snapshot.entries).toSorted()[0] ?? ""; } source = selectedPath ? treeState.snapshot.entries[selectedPath].content : ""; + baseSnapshot = structuredClone(treeState.snapshot); baseRevision = treeState.snapshot.revision; baseDigest = treeState.snapshot.digest; await toolchain?.setSnapshot(treeState.snapshot); @@ -65,13 +69,10 @@ 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; + if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate); + preflightDigest = ""; } async function select(path: string) { @@ -144,6 +145,7 @@ toolchain_fingerprint: treeState.contract.fingerprint, }); diagnostics = []; + preflightDigest = candidate.snapshot.digest; status = `Preview valid · projection ${candidate.evaluation.projection_digest.slice(0, 20)}…`; } catch (error) { status = String(error); @@ -159,6 +161,10 @@ status = "No draft changes to commit."; return; } + if (preflightDigest !== treeState.snapshot.digest) { + status = "Preview the complete candidate successfully before Commit."; + return; + } busy = true; try { treeState = await commitConfigTree(workspaceId, { @@ -169,6 +175,8 @@ toolchain_fingerprint: treeState.contract.fingerprint, }); draftChanges = []; + preflightDigest = ""; + baseSnapshot = structuredClone(treeState.snapshot); baseRevision = treeState.snapshot.revision; baseDigest = treeState.snapshot.digest; await toolchain?.setSnapshot(treeState.snapshot); @@ -198,9 +206,10 @@ 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 }; + if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate); + preflightDigest = ""; selectedPath = Object.keys(candidate.entries).toSorted()[0] ?? ""; source = selectedPath ? candidate.entries[selectedPath].content : ""; renamePath = selectedPath; @@ -218,9 +227,10 @@ to, expected_digest: selected.content_digest, }; - draftChanges = [...draftChanges.filter((item) => !changeTouches(item, selectedPath)), change]; const candidate = await toolchain.applyChanges([change]); treeState = { ...treeState, snapshot: candidate }; + if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate); + preflightDigest = ""; selectedPath = to; source = candidate.entries[to]?.content ?? ""; status = `Rename to ${to} staged. Preview and Commit to persist.`; @@ -261,7 +271,7 @@ - + diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.d.ts b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.d.ts index 6e3d657c..f6e5ecf0 100644 --- a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.d.ts +++ b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.d.ts @@ -5,7 +5,9 @@ export function analyze_snapshot(snapshot: any, entrypoint: string, source_overr export function apply_changes(changes: any): any; -export function complete_current(entrypoint: string, source: string, utf8_byte_offset: number, explicit: boolean): any; +export function changes_between(base: any, candidate: any): any; + +export function complete_current(entrypoint: string, source: string, utf16_offset: number, explicit: boolean): any; export function evaluate_current(contract: any): any; @@ -23,6 +25,7 @@ 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 changes_between: (a: any, b: 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]; diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.js b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.js index f12b9440..713887df 100644 --- a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.js +++ b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm.js @@ -30,19 +30,32 @@ export function apply_changes(changes) { return takeFromExternrefTable0(ret[0]); } +/** + * @param {any} base + * @param {any} candidate + * @returns {any} + */ +export function changes_between(base, candidate) { + const ret = wasm.changes_between(base, candidate); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); +} + /** * @param {string} entrypoint * @param {string} source - * @param {number} utf8_byte_offset + * @param {number} utf16_offset * @param {boolean} explicit * @returns {any} */ -export function complete_current(entrypoint, source, utf8_byte_offset, explicit) { +export function complete_current(entrypoint, source, utf16_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); + const ret = wasm.complete_current(ptr0, len0, ptr1, len1, utf16_offset, explicit); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); } diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm index 7b59d0fa..9ca6bd64 100644 Binary files a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm and b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm differ diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm.d.ts b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm.d.ts index 85f6acbf..70f74b45 100644 --- a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm.d.ts +++ b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm.d.ts @@ -3,6 +3,7 @@ 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 changes_between: (a: any, b: 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]; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigCommitRequest.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigCommitRequest.ts new file mode 100644 index 00000000..ab262091 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigCommitRequest.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigTreeChange } from "./ConfigTreeChange"; +import type { VirtualPath } from "./VirtualPath"; + +export type ConfigCommitRequest = { base_revision: number, base_digest: string, changes: Array, entrypoints: Array, toolchain_fingerprint: string, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigContentType.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigContentType.ts new file mode 100644 index 00000000..bef199f9 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigContentType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ConfigContentType = "decodal" | "text"; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigDiagnostic.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigDiagnostic.ts new file mode 100644 index 00000000..f1de18e3 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigDiagnostic.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigDiagnosticLabel } from "./ConfigDiagnosticLabel"; +import type { ConfigSpan } from "./ConfigSpan"; +import type { VirtualPath } from "./VirtualPath"; + +export type ConfigDiagnostic = { path: VirtualPath, revision: number, tree_digest: string, kind: string, span: ConfigSpan, message: string, labels: Array, notes: Array, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigDiagnosticLabel.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigDiagnosticLabel.ts new file mode 100644 index 00000000..487335f0 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigDiagnosticLabel.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigSpan } from "./ConfigSpan"; + +export type ConfigDiagnosticLabel = { span: ConfigSpan, message: string, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigEntry.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigEntry.ts new file mode 100644 index 00000000..1e16ad74 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigEntry.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigContentType } from "./ConfigContentType"; +import type { VirtualPath } from "./VirtualPath"; + +export type ConfigEntry = { path: VirtualPath, content_type: ConfigContentType, content: string, content_digest: string, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigPreviewRequest.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigPreviewRequest.ts new file mode 100644 index 00000000..6bf8acb1 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigPreviewRequest.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigTreeChange } from "./ConfigTreeChange"; +import type { VirtualPath } from "./VirtualPath"; + +export type ConfigPreviewRequest = { changes: Array, entrypoints: Array, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigSpan.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigSpan.ts new file mode 100644 index 00000000..fb28eb7d --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigSpan.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ConfigSpan = { start_byte: number, end_byte: number, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigTreeChange.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigTreeChange.ts new file mode 100644 index 00000000..9f2442c4 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigTreeChange.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigContentType } from "./ConfigContentType"; +import type { VirtualPath } from "./VirtualPath"; + +export type ConfigTreeChange = { "kind": "create", path: VirtualPath, content_type: ConfigContentType, content: string, } | { "kind": "update", path: VirtualPath, expected_digest: string, content: string, } | { "kind": "rename", from: VirtualPath, to: VirtualPath, expected_digest: string, } | { "kind": "delete", path: VirtualPath, expected_digest: string, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigTreeSnapshot.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigTreeSnapshot.ts new file mode 100644 index 00000000..5f723ce9 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigTreeSnapshot.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigEntry } from "./ConfigEntry"; +import type { VirtualPath } from "./VirtualPath"; + +export type ConfigTreeSnapshot = { revision: number, digest: string, entries: { [key in VirtualPath]: ConfigEntry }, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/EvaluatedConfigCandidate.ts b/web/workspace/src/lib/workspace/config-source/generated/types/EvaluatedConfigCandidate.ts new file mode 100644 index 00000000..f71ba29a --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/EvaluatedConfigCandidate.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigTreeSnapshot } from "./ConfigTreeSnapshot"; +import type { EvaluationResult } from "./EvaluationResult"; +import type { ToolchainContract } from "./ToolchainContract"; + +export type EvaluatedConfigCandidate = { base_revision: number, base_digest: string, snapshot: ConfigTreeSnapshot, contract: ToolchainContract, evaluation: EvaluationResult, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/EvaluatedProjection.ts b/web/workspace/src/lib/workspace/config-source/generated/types/EvaluatedProjection.ts new file mode 100644 index 00000000..86f4d2e1 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/EvaluatedProjection.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { VirtualPath } from "./VirtualPath"; + +export type EvaluatedProjection = { entrypoint: VirtualPath, data_json: unknown, projection_digest: string, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/EvaluationResult.ts b/web/workspace/src/lib/workspace/config-source/generated/types/EvaluationResult.ts new file mode 100644 index 00000000..b2061ee9 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/EvaluationResult.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EvaluatedProjection } from "./EvaluatedProjection"; + +export type EvaluationResult = { projections: Array, projection_digest: string, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ToolchainContract.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ToolchainContract.ts new file mode 100644 index 00000000..66f3d3c6 --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ToolchainContract.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { VirtualPath } from "./VirtualPath"; + +export type ToolchainContract = { contract_version: number, decodal_version: string, schema_version: number, entrypoints: Array, import_policy_version: number, fingerprint: string, }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/VirtualPath.ts b/web/workspace/src/lib/workspace/config-source/generated/types/VirtualPath.ts new file mode 100644 index 00000000..12c7d5ec --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/VirtualPath.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type VirtualPath = string; diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/WorkspaceConfigState.ts b/web/workspace/src/lib/workspace/config-source/generated/types/WorkspaceConfigState.ts new file mode 100644 index 00000000..592277cc --- /dev/null +++ b/web/workspace/src/lib/workspace/config-source/generated/types/WorkspaceConfigState.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfigTreeSnapshot } from "./ConfigTreeSnapshot"; +import type { ToolchainContract } from "./ToolchainContract"; + +export type WorkspaceConfigState = { snapshot: ConfigTreeSnapshot, contract: ToolchainContract, projection_digest: string, }; diff --git a/web/workspace/src/lib/workspace/config-source/toolchain.ts b/web/workspace/src/lib/workspace/config-source/toolchain.ts index d1164e64..cf27ed0f 100644 --- a/web/workspace/src/lib/workspace/config-source/toolchain.ts +++ b/web/workspace/src/lib/workspace/config-source/toolchain.ts @@ -4,6 +4,7 @@ import type { ConfigSourceWorkerRequest, ConfigSourceWorkerResponse } from "./to type Command = | Omit, "id"> | Omit, "id"> + | Omit, "id"> | Omit, "id"> | Omit, "id"> | Omit, "id"> @@ -31,14 +32,17 @@ export class ConfigSourceToolchain { applyChanges(changes: ConfigTreeChange[]): Promise { return this.#request({ kind: "apply_changes", changes }); } + changesBetween(base: ConfigTreeSnapshot, candidate: ConfigTreeSnapshot): Promise { + return this.#request({ kind: "changes_between", base, candidate }); + } analyze(path: string, source?: string): Promise { 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 { - return this.#request({ kind: "complete", path, source, utf8ByteOffset, explicit }); + complete(path: string, source: string, utf16Offset: number, explicit = false): Promise { + return this.#request({ kind: "complete", path, source, utf16Offset, explicit }); } format(source: string): Promise { return this.#request({ kind: "format", source }); diff --git a/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts b/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts index 9ca865f4..32885cb7 100644 --- a/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts +++ b/web/workspace/src/lib/workspace/config-source/toolchain.worker.ts @@ -1,6 +1,7 @@ import init, { analyze_snapshot, apply_changes, + changes_between, complete_current, evaluate_current, format_source, @@ -11,9 +12,10 @@ import type { ConfigTreeChange } from "./types.ts"; export type ConfigSourceWorkerRequest = | { id: number; kind: "set_snapshot"; snapshot: unknown } | { id: number; kind: "apply_changes"; changes: ConfigTreeChange[] } + | { id: number; kind: "changes_between"; base: unknown; candidate: unknown } | { 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: "complete"; path: string; source: string; utf16Offset: number; explicit: boolean } | { id: number; kind: "format"; source: string }; export type ConfigSourceWorkerResponse = @@ -38,6 +40,9 @@ self.onmessage = async (event: MessageEvent): Promise snapshot = apply_changes(request.changes); result = snapshot; break; + case "changes_between": + result = changes_between(request.base, request.candidate); + break; case "analyze": if (!snapshot) throw new Error("config source snapshot is not initialized"); result = analyze_snapshot(snapshot, request.path, request.source); @@ -46,7 +51,7 @@ self.onmessage = async (event: MessageEvent): Promise result = evaluate_current(request.contract); break; case "complete": - result = complete_current(request.path, request.source, request.utf8ByteOffset, request.explicit); + result = complete_current(request.path, request.source, request.utf16Offset, request.explicit); break; case "format": result = format_source(request.source); diff --git a/web/workspace/src/lib/workspace/config-source/types.ts b/web/workspace/src/lib/workspace/config-source/types.ts index f157a515..f8fa6ac8 100644 --- a/web/workspace/src/lib/workspace/config-source/types.ts +++ b/web/workspace/src/lib/workspace/config-source/types.ts @@ -1,93 +1,16 @@ -export type VirtualPath = string; - -export type ConfigContentType = "decodal" | "text"; - -export interface ConfigEntry { - path: VirtualPath; - content_type: ConfigContentType; - content: string; - content_digest: string; -} - -export interface ConfigTreeSnapshot { - revision: number; - digest: string; - entries: Record; -} - -export type ConfigTreeChange = - | { - kind: "create"; - path: VirtualPath; - content_type: ConfigContentType; - content: string; - } - | { - kind: "update"; - path: VirtualPath; - expected_digest: string; - content: string; - } - | { - kind: "rename"; - from: VirtualPath; - to: VirtualPath; - expected_digest: string; - } - | { - kind: "delete"; - path: VirtualPath; - expected_digest: string; - }; - -export interface ToolchainContract { - contract_version: number; - decodal_version: string; - schema_version: number; - entrypoints: VirtualPath[]; - import_policy_version: number; - fingerprint: string; -} - -export interface ConfigDiagnostic { - path: VirtualPath; - revision: number; - tree_digest: string; - kind: string; - span: { start_byte: number; end_byte: number }; - message: string; - labels: Array<{ - span: { start_byte: number; end_byte: number }; - message: string; - }>; - notes: string[]; -} - -export interface WorkspaceConfigTreeResponse { - snapshot: ConfigTreeSnapshot; - contract: ToolchainContract; - projection_digest: string; -} - -export interface EvaluatedConfigCandidate { - base_revision: number; - base_digest: string; - snapshot: ConfigTreeSnapshot; - contract: ToolchainContract; - evaluation: { - projections: Array<{ - entrypoint: VirtualPath; - data_json: unknown; - projection_digest: string; - }>; - projection_digest: string; - }; -} - -export interface ConfigCommitRequest { - base_revision: number; - base_digest: string; - changes: ConfigTreeChange[]; - entrypoints: VirtualPath[]; - toolchain_fingerprint: string; -} +// Core and HTTP DTOs are generated from Rust with ts-rs. +export type { ConfigContentType } from "./generated/types/ConfigContentType.ts"; +export type { ConfigDiagnostic } from "./generated/types/ConfigDiagnostic.ts"; +export type { ConfigDiagnosticLabel } from "./generated/types/ConfigDiagnosticLabel.ts"; +export type { ConfigEntry } from "./generated/types/ConfigEntry.ts"; +export type { ConfigSpan } from "./generated/types/ConfigSpan.ts"; +export type { ConfigTreeChange } from "./generated/types/ConfigTreeChange.ts"; +export type { ConfigTreeSnapshot } from "./generated/types/ConfigTreeSnapshot.ts"; +export type { EvaluatedProjection } from "./generated/types/EvaluatedProjection.ts"; +export type { EvaluationResult } from "./generated/types/EvaluationResult.ts"; +export type { ToolchainContract } from "./generated/types/ToolchainContract.ts"; +export type { VirtualPath } from "./generated/types/VirtualPath.ts"; +export type { ConfigCommitRequest } from "./generated/types/ConfigCommitRequest.ts"; +export type { ConfigPreviewRequest } from "./generated/types/ConfigPreviewRequest.ts"; +export type { EvaluatedConfigCandidate } from "./generated/types/EvaluatedConfigCandidate.ts"; +export type { WorkspaceConfigState as WorkspaceConfigTreeResponse } from "./generated/types/WorkspaceConfigState.ts"; diff --git a/web/workspace/src/lib/workspace/settings/DecodalSourceEditor.svelte b/web/workspace/src/lib/workspace/settings/DecodalSourceEditor.svelte index 6a9411d0..20e58e61 100644 --- a/web/workspace/src/lib/workspace/settings/DecodalSourceEditor.svelte +++ b/web/workspace/src/lib/workspace/settings/DecodalSourceEditor.svelte @@ -16,7 +16,7 @@ readonly?: boolean; ariaLabel?: string; onChange?: (value: string) => void; - onComplete?: (source: string, utf8ByteOffset: number, explicit: boolean) => Promise; + onComplete?: (source: string, utf16Offset: number, explicit: boolean) => Promise; } = $props(); let host = $state(null); @@ -54,8 +54,7 @@ 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); + return await handleComplete(doc, context.pos, context.explicit); }] })] : []), keymap.of([]), EditorState.readOnly.of(initialReadonly),