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"))
29 .map_err(|e| err_str(e))?;
30 if v.is_null() || v.is_undefined() {
31 return Err("cm6 bundle not loaded".to_string());
32 }
33 Ok(v)
34}
35
36/// Call `method` on `g` with the given args.
37fn call(g: &JsValue, method: &str, args: &[&JsValue]) -> Result<JsValue, String> {
38 let m = js_sys::Reflect::get(g, &JsValue::from_str(method)).map_err(|e| err_str(e))?;
39 let f: js_sys::Function = m.unchecked_into();
40 let arr = js_sys::Array::new();
41 for a in args {
42 arr.push(a);
43 }
44 js_sys::Reflect::apply(&f, g, &arr).map_err(|e| err_str(e))
45}
46
47/// Can this file name be shown in the text editor? (Delegated to the bundle so
48/// the extension list lives in one place.)
49pub fn is_text(name: &str) -> bool {
50 global()
51 .ok()
52 .and_then(|g| call(&g, "isText", &[&JsValue::from_str(name)]).ok())
53 .and_then(|v| v.as_bool())
54 .unwrap_or(false)
55}
56
57/// Create an editor inside `container`.
58pub fn create(
59 container: &web_sys::Element,
60 value: &str,
61 filename: &str,
62 editable: bool,
63) -> Result<Cm, String> {
64 let g = global()?;
65 let opts = js_sys::Object::new();
66 js_sys::Reflect::set(&opts, &JsValue::from_str("value"), &JsValue::from_str(value))
67 .map_err(|e| err_str(e))?;
68 js_sys::Reflect::set(&opts, &JsValue::from_str("filename"), &JsValue::from_str(filename))
69 .map_err(|e| err_str(e))?;
70 js_sys::Reflect::set(&opts, &JsValue::from_str("editable"), &JsValue::from_bool(editable))
71 .map_err(|e| err_str(e))?;
72
73 let view = call(
74 &g,
75 "create",
76 &[
77 &JsValue::from(container),
78 &JsValue::from(&opts),
79 ],
80 )?;
81 Ok(Cm { view })
82}
83
84pub fn destroy(view: &JsValue) {
85 let Ok(g) = global() else { return };
86 let _ = call(&g, "destroy", &[view]);
87}
88
89/// Replace the whole document (used when (re)loading a file). Used from the
90/// editor (milestone 5).
91#[allow(dead_code)]
92pub fn set_value(view: &JsValue, text: &str) {
93 let Ok(g) = global() else { return };
94 let _ = call(&g, "setValue", &[view, &JsValue::from_str(text)]);
95}
96
97/// Read the current document text (used by the editor's save path, milestone
98/// 5).
99#[allow(dead_code)]
100pub fn get_value(view: &JsValue) -> Option<String> {
101 let g = global().ok()?;
102 call(&g, "getValue", &[view]).ok()?.as_string()
103}
104