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//!
8//! The bundle is a separate `<script>`, so it may be missing (a failed load,
9//! a stale cache). Every binding is therefore `catch`: a missing global
10//! surfaces as an `Err` instead of a wasm trap.
11
12use wasm_bindgen::JsCast;
13use wasm_bindgen::prelude::*;
14
15#[wasm_bindgen]
16extern "C" {
17 #[wasm_bindgen(js_namespace = __fbng_cm, js_name = create, catch)]
18 fn js_create(
19 container: &web_sys::Element,
20 value: &str,
21 filename: &str,
22 editable: bool,
23 on_update: Option<js_sys::Function>,
24 ) -> Result<JsValue, JsValue>;
25
26 #[wasm_bindgen(js_namespace = __fbng_cm, js_name = destroy, catch)]
27 fn js_destroy(view: &JsValue) -> Result<(), JsValue>;
28
29 #[wasm_bindgen(js_namespace = __fbng_cm, js_name = setValue, catch)]
30 fn js_set_value(view: &JsValue, text: &str) -> Result<(), JsValue>;
31
32 #[wasm_bindgen(js_namespace = __fbng_cm, js_name = getValue, catch)]
33 fn js_get_value(view: &JsValue) -> Result<JsValue, JsValue>;
34
35 #[wasm_bindgen(js_namespace = __fbng_cm, js_name = focus, catch)]
36 fn js_focus(view: &JsValue) -> Result<(), JsValue>;
37}
38
39/// A live CodeMirror editor. Destroy it with [`destroy`] when the host goes
40/// away (we do that in a Leptos `on_cleanup`).
41pub struct Cm {
42 pub view: JsValue,
43}
44
45/// Extract a human-readable message from a JS error value.
46fn err_str(e: JsValue) -> String {
47 e.dyn_into::<js_sys::Error>()
48 .ok()
49 .and_then(|e| e.message().as_string())
50 .unwrap_or_else(|| "editor error".to_string())
51}
52
53/// Create an editor inside `container`.
54///
55/// `on_update` is an optional JS function called whenever the document changes
56/// (used to track the editor's dirty state). If provided, the caller must keep
57/// the backing `Closure` alive for as long as the editor exists.
58pub fn create(
59 container: &web_sys::Element,
60 value: &str,
61 filename: &str,
62 editable: bool,
63 on_update: Option<&JsValue>,
64) -> Result<Cm, String> {
65 let on_update = on_update.map(|f| f.clone().unchecked_into::<js_sys::Function>());
66 js_create(container, value, filename, editable, on_update)
67 .map(|view| Cm { view })
68 .map_err(err_str)
69}
70
71pub fn destroy(view: &JsValue) {
72 let _ = js_destroy(view);
73}
74
75/// Replace the whole document (used when (re)loading a file).
76pub fn set_value(view: &JsValue, text: &str) {
77 let _ = js_set_value(view, text);
78}
79
80/// Read the current document text (used by the editor's save path).
81pub fn get_value(view: &JsValue) -> Option<String> {
82 js_get_value(view).ok()?.as_string()
83}
84
85/// Focus the editor (so the caret is ready for typing on open).
86pub fn focus(view: &JsValue) {
87 let _ = js_focus(view);
88}
89