Skip to main content

yggdrasil/bridges/
codemirror.rs

1//! CodeMirror 编辑器的 wasm-bindgen 绑定层。
2//!
3//! 封装与 `window.CodeMirrorEditor`(IIFE 暴露的全局对象字面量)的全部交互,
4//! 严格镜像 [`crate::bridges::tiptap`] 的结构:wasm-bindgen extern +
5//! `EditorHandle` 仅在 WASM 前端编译(server 构建无 window)。SQL 补全用的
6//! `SqlSchema`/`SqlTable` 是 server function 返回值 DTO,定义在
7//! [`crate::models::sql_schema`](不属于本文件——避免 API 层反向依赖桥接层)。
8//!
9//! 与 tiptap 一样,`create` 以 js_namespace 绑定为成员调用,调用时才解析全局;
10//! 脚本未加载时返回 Err。
11//!
12//! 用法(Leptos):挂载点用 `NodeRef` 取元素,先用 `use_browser_library(BrowserLibrary::CodeMirror, ..)` 等库就绪,
13//! 再在 `Effect` 内构造 `EditorOptions` 并 `create(&element, &opts)`;`EditorHandle` 含 JS 引用
14//! (非 `Send + Sync`),放进 `StoredValue::new_local`/`RwSignal::new_local`,`on_cleanup` 里 `take`
15//! 出来丢弃即触发 `Drop` → `destroy`。
16
17// ============================================================================
18// 以下全部仅在 WASM 前端编译:wasm-bindgen extern + EditorHandle + 闭包。
19// 放在 #[cfg] 子模块内,避免 server 构建尝试编译引用 JS 对象的 extern。
20// ============================================================================
21#[cfg(target_arch = "wasm32")]
22pub mod wasm {
23    use wasm_bindgen::prelude::*;
24    use wasm_bindgen::JsCast;
25
26    // —— window.CodeMirrorEditor.create ——
27    //
28    // `js_namespace` 把 `create` 绑成 `CodeMirrorEditor.create(...)` 的成员调用,
29    // 全局在调用时才解析:脚本未加载时返回 Err(TypeError),而不是在绑定阶段 panic。
30    #[wasm_bindgen]
31    extern "C" {
32        /// `CodeMirrorEditor.create(container, options)`:在元素上创建实例,替换该元素上的旧实例。
33        /// 构造失败抛异常(被 catch 捕获)。
34        #[wasm_bindgen(js_namespace = CodeMirrorEditor, js_name = create, catch)]
35        pub fn create(
36            container: &web_sys::HtmlElement,
37            options: &EditorOptions,
38        ) -> Result<EditorInstance, JsValue>;
39    }
40
41    // —— 编辑器实例(CodeMirrorInstance)——
42    #[wasm_bindgen]
43    extern "C" {
44        /// `CodeMirrorEditor.create` 返回的编辑器实例对象,承载 CodeMirror EditorView。
45        pub type EditorInstance;
46
47        /// 返回当前文档全文。
48        #[wasm_bindgen(method, js_name = getValue)]
49        pub fn get_value(this: &EditorInstance) -> String;
50
51        /// 替换整个文档内容(dispatch changes,触发 onChange)。
52        #[wasm_bindgen(method, js_name = setValue)]
53        pub fn set_value(this: &EditorInstance, s: &str);
54
55        /// 热切换主题(Compartment.reconfigure,不重建实例)。
56        #[wasm_bindgen(method, js_name = setTheme)]
57        pub fn set_theme(this: &EditorInstance, theme: &str);
58
59        /// 热切换 Vim 模式(Compartment.reconfigure,不重建实例)。
60        #[wasm_bindgen(method, js_name = setVim)]
61        pub fn set_vim(this: &EditorInstance, v: bool);
62
63        /// 热切换语言(go/rust/python/node/javascript/sql,Compartment.reconfigure)。
64        /// 由 CodeRunner 组件在挂载时按 data-lang 调用。
65        #[wasm_bindgen(method, js_name = setLanguage)]
66        pub fn set_language(this: &EditorInstance, lang: &str);
67
68        /// 更新 SQL 补全 schema(Compartment.reconfigure)。
69        /// 参数为 serde_wasm_bindgen::to_value 序列化后的 JsValue
70        ///(SqlSchema 是 serde 类型,非 wasm-bindgen 类型,故不能直接传 &SqlSchema)。
71        #[wasm_bindgen(method, js_name = setSchema)]
72        pub fn set_schema(this: &EditorInstance, schema: &wasm_bindgen::JsValue);
73
74        /// 让编辑器获取焦点。
75        #[wasm_bindgen(method)]
76        pub fn focus(this: &EditorInstance);
77
78        /// 销毁编辑器,释放 JS 侧资源。
79        #[wasm_bindgen(method)]
80        pub fn destroy(this: &EditorInstance);
81    }
82
83    // —— EditorOptions:用 builder 模式(setter)构造 JS 对象 ——
84    #[wasm_bindgen]
85    extern "C" {
86        /// 传给 `CodeMirrorEditor.create` 的配置对象,对应 JS 侧的 EditorOptions。
87        /// 用 `default()` 创建空对象后通过 setter 设置字段。
88        pub type EditorOptions;
89
90        /// 语言(默认 'sql')。
91        #[wasm_bindgen(method, setter, js_name = language)]
92        pub fn set_language(this: &EditorOptions, v: &str);
93
94        /// 主题:'light' 或 'dark'(配色取自 `CODE_PALETTES`)。
95        #[wasm_bindgen(method, setter, js_name = theme)]
96        pub fn set_theme(this: &EditorOptions, v: &str);
97
98        /// 是否启用 Vim keymap。
99        #[wasm_bindgen(method, setter, js_name = vim)]
100        pub fn set_vim(this: &EditorOptions, v: bool);
101
102        /// SQL 补全 schema(表/列数据)。v 为 serde_wasm_bindgen::to_value 序列化结果。
103        #[wasm_bindgen(method, setter, js_name = schema)]
104        pub fn set_schema(this: &EditorOptions, v: &wasm_bindgen::JsValue);
105
106        /// 初始文档内容。
107        #[wasm_bindgen(method, setter, js_name = value)]
108        pub fn set_value(this: &EditorOptions, v: &str);
109
110        /// 文档变更回调(参数为最新全文)。
111        #[wasm_bindgen(method, setter, js_name = onChange)]
112        pub fn set_on_change(this: &EditorOptions, cb: &Closure<dyn FnMut(String)>);
113
114        /// 编辑器就绪回调(构造末尾同步触发一次)。
115        #[wasm_bindgen(method, setter, js_name = onReady)]
116        pub fn set_on_ready(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
117
118        /// Ctrl/Cmd + Enter 快捷键回调(SQL 控制台、可运行代码块触发执行)。
119        #[wasm_bindgen(method, setter, js_name = onRunShortcut)]
120        pub fn set_on_run_shortcut(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
121
122        /// Vim 模式变化回调(normal / insert / visual / replace)。
123        #[wasm_bindgen(method, setter, js_name = onVimModeChange)]
124        pub fn set_on_vim_mode_change(this: &EditorOptions, cb: &Closure<dyn FnMut(String)>);
125    }
126
127    impl Default for EditorOptions {
128        /// 空 JS 对象;字段只经上面的 setter 写入,JS 端按同名 interface 读取。
129        fn default() -> Self {
130            js_sys::Object::new().unchecked_into()
131        }
132    }
133
134    /// 编辑器实例句柄:持有 instance + 所有 Closure,Drop 时销毁实例并释放闭包。
135    ///
136    /// 闭包字段 `_` 前缀表示仅用于保持生命周期——它们被注入 JS 后,JS 侧持有
137    /// 函数引用;只要 [`EditorHandle`] 存活,闭包就不会被回收。Drop 时随结构释放。
138    pub struct EditorHandle {
139        instance: EditorInstance,
140        _on_change: Closure<dyn FnMut(String)>,
141        _on_ready: Closure<dyn FnMut()>,
142        _on_run_shortcut: Closure<dyn FnMut()>,
143        _on_vim_mode_change: Option<Closure<dyn FnMut(String)>>,
144    }
145
146    impl EditorHandle {
147        /// 调用方须先把各 closure set 进 EditorOptions,再 create,
148        /// 然后把返回的 instance + 同名 closure 一起传入 new。
149        /// `on_run_shortcut` 对应 Ctrl/Cmd+Enter 回调;不用该功能时传 no-op 闭包。
150        pub fn new(
151            instance: EditorInstance,
152            on_change: Closure<dyn FnMut(String)>,
153            on_ready: Closure<dyn FnMut()>,
154            on_run_shortcut: Closure<dyn FnMut()>,
155        ) -> Self {
156            Self {
157                instance,
158                _on_change: on_change,
159                _on_ready: on_ready,
160                _on_run_shortcut: on_run_shortcut,
161                _on_vim_mode_change: None,
162            }
163        }
164
165        /// 一并持有已注入 EditorOptions 的 Vim 模式回调。
166        pub fn with_vim_mode(mut self, on_vim_mode_change: Closure<dyn FnMut(String)>) -> Self {
167            self._on_vim_mode_change = Some(on_vim_mode_change);
168            self
169        }
170
171        /// 借用底层实例,供宿主调 getValue/setTheme/setSchema 等。
172        pub fn instance(&self) -> &EditorInstance {
173            &self.instance
174        }
175    }
176
177    impl Drop for EditorHandle {
178        fn drop(&mut self) {
179            // 销毁 JS 侧编辑器;随后 _on_change/_on_ready 字段按声明顺序释放,
180            // 释放 wasm-bindgen 函数表槽位。
181            self.instance.destroy();
182        }
183    }
184}
185
186#[cfg(target_arch = "wasm32")]
187pub use wasm::*;