use config_source::{ ConfigSchemaContribution, ConfigTreeChange, ConfigTreeSnapshot, EvaluationResult, SnapshotEnvironment, ToolchainContract, VirtualPath, WorkspaceConfigSchemaBundle, }; use serde_wasm_bindgen::{Serializer, from_value}; use std::cell::RefCell; use wasm_bindgen::prelude::*; thread_local! { static SESSION: RefCell> = const { RefCell::new(None) }; } #[wasm_bindgen] pub fn compose_schema_bundle(contributions: JsValue) -> Result { let contributions: Vec = decode(contributions)?; encode(WorkspaceConfigSchemaBundle::compose(contributions).map_err(js_error)?) } #[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 { let changes: Vec = 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 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)?; 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, } #[derive(serde::Serialize)] struct WasmCompletionItem { label: String, kind: String, detail: Option, priority: i32, } #[wasm_bindgen] pub fn complete_current( entrypoint: String, source: String, utf16_offset: usize, explicit: bool, ) -> Result { 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 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:?}")))? .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] pub fn evaluate_snapshot(snapshot: JsValue, contract: JsValue) -> Result { let snapshot: ConfigTreeSnapshot = decode(snapshot)?; let contract: ToolchainContract = decode(contract)?; encode( SnapshotEnvironment::new(snapshot) .evaluate_contract(&contract) .map_err(|diagnostics| { encode(&diagnostics).unwrap_or_else(|_| JsValue::from_str("evaluation failed")) })?, ) } #[wasm_bindgen] pub fn analyze_snapshot( snapshot: JsValue, entrypoint: String, source_override: Option, ) -> Result { let snapshot: ConfigTreeSnapshot = decode(snapshot)?; let entrypoint = VirtualPath::parse(entrypoint).map_err(js_error)?; encode(SnapshotEnvironment::new(snapshot).analyze(&entrypoint, source_override.as_deref())) } #[wasm_bindgen] pub fn format_source(source: String) -> Result { SnapshotEnvironment::new(ConfigTreeSnapshot::empty()) .format(&source) .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())) } fn encode(value: T) -> Result { value .serialize(&Serializer::json_compatible()) .map_err(|error| JsValue::from_str(&error.to_string())) } fn js_error(error: impl std::fmt::Display) -> JsValue { JsValue::from_str(&error.to_string()) } #[allow(dead_code)] fn _assert_serializable(_: EvaluationResult) {}