//! Thin, swappable bindings to the vendored CodeMirror 6 bundle. //! //! The bundle (`web/cm6.js`, built from `web/cm/wrapper.js`) installs a //! `window.__fbng_cm` global. This module is the *only* place that knows how //! to talk to it, so swapping the editor later means re-implementing just //! these functions. use wasm_bindgen::{JsCast, JsValue}; /// A live CodeMirror editor. Destroy it with [`destroy`] when the host goes /// away (we do that in a Leptos `on_cleanup`). pub struct Cm { pub view: JsValue, } /// Extract a human-readable message from a JS error value. fn err_str(e: JsValue) -> String { e.clone() .dyn_into::() .ok() .and_then(|e| e.message().as_string()) .unwrap_or_else(|| "editor error".to_string()) } fn global() -> Result { let w = web_sys::window().ok_or("no window".to_string())?; let wjs: JsValue = w.into(); let v = js_sys::Reflect::get(&wjs, &JsValue::from_str("__fbng_cm")).map_err(err_str)?; if v.is_null() || v.is_undefined() { return Err("cm6 bundle not loaded".to_string()); } Ok(v) } /// Call `method` on `g` with the given args. fn call(g: &JsValue, method: &str, args: &[&JsValue]) -> Result { let m = js_sys::Reflect::get(g, &JsValue::from_str(method)).map_err(err_str)?; let f: js_sys::Function = m.unchecked_into(); let arr = js_sys::Array::new(); for a in args { arr.push(a); } js_sys::Reflect::apply(&f, g, &arr).map_err(err_str) } /// Can this file name be shown in the text editor? (Delegated to the bundle so /// the extension list lives in one place.) pub fn is_text(name: &str) -> bool { global() .ok() .and_then(|g| call(&g, "isText", &[&JsValue::from_str(name)]).ok()) .and_then(|v| v.as_bool()) .unwrap_or(false) } /// Create an editor inside `container`. /// /// `on_update` is an optional JS function called whenever the document changes /// (used to track the editor's dirty state). If provided, the caller must keep /// the backing `Closure` alive for as long as the editor exists. pub fn create( container: &web_sys::Element, value: &str, filename: &str, editable: bool, on_update: Option<&JsValue>, ) -> Result { let g = global()?; let opts = js_sys::Object::new(); js_sys::Reflect::set( &opts, &JsValue::from_str("value"), &JsValue::from_str(value), ) .map_err(err_str)?; js_sys::Reflect::set( &opts, &JsValue::from_str("filename"), &JsValue::from_str(filename), ) .map_err(err_str)?; js_sys::Reflect::set( &opts, &JsValue::from_str("editable"), &JsValue::from_bool(editable), ) .map_err(err_str)?; if let Some(f) = on_update { js_sys::Reflect::set(&opts, &JsValue::from_str("onUpdate"), f).map_err(err_str)?; } let view = call( &g, "create", &[&JsValue::from(container), &JsValue::from(&opts)], )?; Ok(Cm { view }) } pub fn destroy(view: &JsValue) { let Ok(g) = global() else { return }; let _ = call(&g, "destroy", &[view]); } /// Replace the whole document (used when (re)loading a file). Used from the /// editor (milestone 5). pub fn set_value(view: &JsValue, text: &str) { let Ok(g) = global() else { return }; let _ = call(&g, "setValue", &[view, &JsValue::from_str(text)]); } /// Read the current document text (used by the editor's save path, milestone /// 5). pub fn get_value(view: &JsValue) -> Option { let g = global().ok()?; call(&g, "getValue", &[view]).ok()?.as_string() } /// Focus the editor (so the caret is ready for typing on open). pub fn focus(view: &JsValue) { let Ok(g) = global() else { return }; let _ = call(&g, "focus", &[view]); }