Files
yoi/crates/config-source-wasm/src/lib.rs
T

179 lines
5.9 KiB
Rust

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<Option<ConfigTreeSnapshot>> = const { RefCell::new(None) };
}
#[wasm_bindgen]
pub fn compose_schema_bundle(contributions: JsValue) -> Result<JsValue, JsValue> {
let contributions: Vec<ConfigSchemaContribution> = 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<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 changes_between(base: JsValue, candidate: JsValue) -> Result<JsValue, JsValue> {
let base: ConfigTreeSnapshot = decode(base)?;
let candidate: ConfigTreeSnapshot = decode(candidate)?;
encode(base.changes_to(&candidate))
}
#[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,
utf16_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 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<JsValue, JsValue> {
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<String>,
) -> Result<JsValue, JsValue> {
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<String, JsValue> {
SnapshotEnvironment::new(ConfigTreeSnapshot::empty())
.format(&source)
.map_err(|error| JsValue::from_str(&error))
}
fn utf16_to_utf8_offset(source: &str, utf16_offset: usize) -> Result<usize, JsValue> {
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<T: serde::de::DeserializeOwned>(value: JsValue) -> Result<T, JsValue> {
from_value(value).map_err(|error| JsValue::from_str(&error.to_string()))
}
fn encode<T: serde::Serialize>(value: T) -> Result<JsValue, JsValue> {
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) {}