cm.rs
⎇
Raw
1//! Thin, swappable bindings to the vendored CodeMirror 6 bundle.
2//!
3//! The bundle (`web/cm6.js`, built from `web/cm/wrapper.js`) installs a
4//! `window.__fbng_cm` global. This module is the *only* place that knows how
5//! to talk to it, so swapping the editor later means re-implementing just
6//! these functions.
7
8use wasm_bindgen::{JsCast, JsValue};
9
10/// A live CodeMirror editor. Destroy it with [`destroy`] when the host goes
11/// away (we do that in a Leptos `on_cleanup`).
12pub struct Cm {
13 pub view: JsValue,
14}
15
16/// Extract a human-readable message from a JS error value.
17fn err_str(e: JsValue) -> String {
18 e.clone()
19 .dyn_into::<js_sys::Error>()
20 .ok()
21 .and_then(|e| e.message().as_string())
22 .unwrap_or_else(|| "editor error".to_string())
23}
24
25fn global() -> Result<JsValue, String> {
26 let w = web_sys::window().ok_or("no window".to_string())?;
27 let wjs: JsValue = w.into();
28 let v = js_sys::Reflect::get(&wjs, &JsValue::from_str("__fbng_cm")).map_err(err_str)?;
29 if v.is_null() || v.is_undefined() {
30 return Err("cm6 bundle not loaded".to_string());
31 }
32 Ok(v)
33}
34
35/// Call `method` on `g` with the given args.
36fn call(g: &JsValue, method: &str, args: &[&JsValue]) -> Result<JsValue, String> {
37 let m = js_sys::Reflect::get(g, &JsValue::from_str(method)).map_err(err_str)?;
38 let f: js_sys::Function = m.unchecked_into();
39 let arr = js_sys::Array::new();
40 for a in args {
41 arr.push(a);
42 }
43 js_sys::Reflect::apply(&f, g, &arr).map_err(err_str)
44}
45
46/// Create an editor inside `container`.
47///
48/// `on_update` is an optional JS function called whenever the document changes
49/// (used to track the editor's dirty state). If provided, the caller must keep
50/// the backing `Closure` alive for as long as the editor exists.
51pub fn create(
52 container: &web_sys::Element,
53 value: &str,
54 filename: &str,
55 editable: bool,
56 on_update: Option<&JsValue>,
57) -> Result<Cm, String> {
58 let g = global()?;
59 let opts = js_sys::Object::new();
60 js_sys::Reflect::set(
61 &opts,
62 &JsValue::from_str("value"),
63 &JsValue::from_str(value),
64 )
65 .map_err(err_str)?;
66 js_sys::Reflect::set(
67 &opts,
68 &JsValue::from_str("filename"),
69 &JsValue::from_str(filename),
70 )
71 .map_err(err_str)?;
72 js_sys::Reflect::set(
73 &opts,
74 &JsValue::from_str("editable"),
75 &JsValue::from_bool(editable),
76 )
77 .map_err(err_str)?;
78 if let Some(f) = on_update {
79 js_sys::Reflect::set(&opts, &JsValue::from_str("onUpdate"), f).map_err(err_str)?;
80 }
81
82 let view = call(
83 &g,
84 "create",
85 &[&JsValue::from(container), &JsValue::from(&opts)],
86 )?;
87 Ok(Cm { view })
88}
89
90pub fn destroy(view: &JsValue) {
91 let Ok(g) = global() else { return };
92 let _ = call(&g, "destroy", &[view]);
93}
94
95/// Replace the whole document (used when (re)loading a file). Used from the
96/// editor (milestone 5).
97pub fn set_value(view: &JsValue, text: &str) {
98 let Ok(g) = global() else { return };
99 let _ = call(&g, "setValue", &[view, &JsValue::from_str(text)]);
100}
101
102/// Read the current document text (used by the editor's save path, milestone
103/// 5).
104pub fn get_value(view: &JsValue) -> Option<String> {
105 let g = global().ok()?;
106 call(&g, "getValue", &[view]).ok()?.as_string()
107}
108
109/// Focus the editor (so the caret is ready for typing on open).
110pub fn focus(view: &JsValue) {
111 let Ok(g) = global() else { return };
112 let _ = call(&g, "focus", &[view]);
113}
114