cm.rs
| 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 | |
| 12 | use wasm_bindgen::JsCast; |
| 13 | use wasm_bindgen::prelude::*; |
| 14 | |
| 15 | #[wasm_bindgen] |
| 16 | extern "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`). |
| 41 | pub struct Cm { |
| 42 | pub view: JsValue, |
| 43 | } |
| 44 | |
| 45 | /// Extract a human-readable message from a JS error value. |
| 46 | fn 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. |
| 58 | pub 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 | |
| 71 | pub fn destroy(view: &JsValue) { |
| 72 | let _ = js_destroy(view); |
| 73 | } |
| 74 | |
| 75 | /// Replace the whole document (used when (re)loading a file). |
| 76 | pub 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). |
| 81 | pub 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). |
| 86 | pub fn focus(view: &JsValue) { |
| 87 | let _ = js_focus(view); |
| 88 | } |
| 89 |