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/// Can this file name be shown in the text editor? (Delegated to the bundle so
47/// the extension list lives in one place.)
48pub fn is_text(name: &str) -> bool {
49 global()
50 .ok()
51 .and_then(|g| call(&g, "isText", &[&JsValue::from_str(name)]).ok())
52 .and_then(|v| v.as_bool())
53 .unwrap_or(false)
54}
55
56/// Create an editor inside `container`.
57///
58/// `on_update` is an optional JS function called whenever the document changes
59/// (used to track the editor's dirty state). If provided, the caller must keep
60/// the backing `Closure` alive for as long as the editor exists.
61pub fn create(
62 container: &web_sys::Element,
63 value: &str,
64 filename: &str,
65 editable: bool,
66 on_update: Option<&JsValue>,
67) -> Result<Cm, String> {
68 let g = global()?;
69 let opts = js_sys::Object::new();
70 js_sys::Reflect::set(
71 &opts,
72 &JsValue::from_str("value"),
73 &JsValue::from_str(value),
74 )
75 .map_err(err_str)?;
76 js_sys::Reflect::set(
77 &opts,
78 &JsValue::from_str("filename"),
79 &JsValue::from_str(filename),
80 )
81 .map_err(err_str)?;
82 js_sys::Reflect::set(
83 &opts,
84 &JsValue::from_str("editable"),
85 &JsValue::from_bool(editable),
86 )
87 .map_err(err_str)?;
88 if let Some(f) = on_update {
89 js_sys::Reflect::set(&opts, &JsValue::from_str("onUpdate"), f).map_err(err_str)?;
90 }
91
92 let view = call(
93 &g,
94 "create",
95 &[&JsValue::from(container), &JsValue::from(&opts)],
96 )?;
97 Ok(Cm { view })
98}
99
100pub fn destroy(view: &JsValue) {
101 let Ok(g) = global() else { return };
102 let _ = call(&g, "destroy", &[view]);
103}
104
105/// Replace the whole document (used when (re)loading a file). Used from the
106/// editor (milestone 5).
107pub fn set_value(view: &JsValue, text: &str) {
108 let Ok(g) = global() else { return };
109 let _ = call(&g, "setValue", &[view, &JsValue::from_str(text)]);
110}
111
112/// Read the current document text (used by the editor's save path, milestone
113/// 5).
114pub fn get_value(view: &JsValue) -> Option<String> {
115 let g = global().ok()?;
116 call(&g, "getValue", &[view]).ok()?.as_string()
117}
118
119/// Focus the editor (so the caret is ready for typing on open).
120pub fn focus(view: &JsValue) {
121 let Ok(g) = global() else { return };
122 let _ = call(&g, "focus", &[view]);
123}
124