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 一样,`CodeMirrorEditor` 是 IIFE 挂在 window 上的**对象字面量**
10//! (`{ create }`),不是函数——因此用 `js_sys::Reflect::get` 做属性访问拿到,
11//! 不能用 wasm-bindgen 的 extern fn(那会被编成函数调用,"not a function")。
12
13// ============================================================================
14// 以下全部仅在 WASM 前端编译:wasm-bindgen extern + EditorHandle + 闭包。
15// 放在 #[cfg] 子模块内,避免 server 构建尝试编译引用 JS 对象的 extern。
16// ============================================================================
17#[cfg(target_arch = "wasm32")]
18pub mod wasm {
19    use wasm_bindgen::prelude::*;
20    use wasm_bindgen::JsCast;
21
22    // —— window.CodeMirrorEditor 模块对象 ——
23    //
24    // CodeMirrorEditor 是 IIFE 产物挂在 window 上的对象字面量(含 create 方法),
25    // 不是函数。wasm-bindgen 对 `fn get_module() -> T` 形式的 extern 会生成
26    // `window.CodeMirrorEditor()`(函数调用),会因 "not a function" 失败。
27    // 因此用 js_sys::Reflect::get 做属性访问拿到模块对象,再 unchecked_into。
28    #[wasm_bindgen]
29    extern "C" {
30        /// `window.CodeMirrorEditor` 模块对象的 Rust 映射(IIFE 产物挂在 window 上的对象字面量)。
31        /// 不是函数——通过 [`get_module`] 用 Reflect::get 取属性而非 extern fn 调用拿到。
32        pub type CodeMirrorEditorModule;
33
34        /// 调用 `CodeMirrorEditor.create(containerId, opts)`。
35        /// 找不到容器返回 null(被 Option 捕获);构造失败抛异常(被 catch 捕获)。
36        #[wasm_bindgen(method, catch)]
37        pub fn create(
38            this: &CodeMirrorEditorModule,
39            container_id: &str,
40            opts: &EditorOptions,
41        ) -> Result<Option<EditorInstance>, JsValue>;
42    }
43
44    /// 读取 `window.CodeMirrorEditor`(IIFE 默认导出,顶层 var 即 window 属性)。
45    /// 用 Reflect::get 做属性访问——extern fn 形式会被 wasm-bindgen 编成函数调用。
46    ///
47    /// 用 unchecked_into 而非 dyn_into:CodeMirrorEditor 是 JS 对象字面量,
48    /// 不是 wasm-bindgen 注册的构造函数实例,dyn_into 的 instanceof 检查必然失败。
49    /// unchecked_into 只做编译期类型标注,不做运行时校验
50    /// (Reflect.get 已保证拿到的是目标对象)。
51    pub fn get_module() -> CodeMirrorEditorModule {
52        // 缺 window:本函数只应在浏览器环境的 wasm32 前端调用,不应在其它上下文触发。
53        let window = web_sys::window().expect("no window: get_module 只能在浏览器 wasm32 前端调用");
54        // 调用方先等待 use_browser_library 就绪,再构造 Options 并调用 create。
55        let val = js_sys::Reflect::get(&window, &"CodeMirrorEditor".into()).expect(
56            "window.CodeMirrorEditor missing: /codemirror/editor.js 未加载,检查该静态资源是否随构建产物部署",
57        );
58        val.unchecked_into::<CodeMirrorEditorModule>()
59    }
60
61    // —— 编辑器实例(CodeMirrorInstance)——
62    #[wasm_bindgen]
63    extern "C" {
64        /// `CodeMirrorEditor.create` 返回的编辑器实例对象,承载 CodeMirror EditorView。
65        pub type EditorInstance;
66
67        /// 返回当前文档全文。
68        #[wasm_bindgen(method, js_name = getValue)]
69        pub fn get_value(this: &EditorInstance) -> String;
70
71        /// 替换整个文档内容(dispatch changes,触发 onChange)。
72        #[wasm_bindgen(method, js_name = setValue)]
73        pub fn set_value(this: &EditorInstance, s: &str);
74
75        /// 热切换主题(Compartment.reconfigure,不重建实例)。
76        #[wasm_bindgen(method, js_name = setTheme)]
77        pub fn set_theme(this: &EditorInstance, theme: &str);
78
79        /// 热切换 Vim 模式(Compartment.reconfigure,不重建实例)。
80        #[wasm_bindgen(method, js_name = setVim)]
81        pub fn set_vim(this: &EditorInstance, v: bool);
82
83        /// 热切换语言(go/rust/python/node/javascript/sql,Compartment.reconfigure)。
84        /// 由 CodeRunner 组件在挂载时按 data-lang 调用。
85        #[wasm_bindgen(method, js_name = setLanguage)]
86        pub fn set_language(this: &EditorInstance, lang: &str);
87
88        /// 更新 SQL 补全 schema(Compartment.reconfigure)。
89        /// 参数为 serde_wasm_bindgen::to_value 序列化后的 JsValue
90        ///(SqlSchema 是 serde 类型,非 wasm-bindgen 类型,故不能直接传 &SqlSchema)。
91        #[wasm_bindgen(method, js_name = setSchema)]
92        pub fn set_schema(this: &EditorInstance, schema: &wasm_bindgen::JsValue);
93
94        /// 让编辑器获取焦点。
95        #[wasm_bindgen(method)]
96        pub fn focus(this: &EditorInstance);
97
98        /// 销毁编辑器,释放 JS 侧资源。
99        #[wasm_bindgen(method)]
100        pub fn destroy(this: &EditorInstance);
101    }
102
103    // —— EditorOptions:用 builder 模式(setter)构造 JS 对象 ——
104    #[wasm_bindgen]
105    extern "C" {
106        /// 传给 `CodeMirrorEditor.create` 的配置对象,对应 JS 侧的 EditorOptions。
107        /// 用 `new()` 创建空对象后通过 setter 链式设置字段。
108        pub type EditorOptions;
109
110        /// 构造一个空的 EditorOptions,随后用各 setter 填充。
111        #[wasm_bindgen(constructor)]
112        pub fn new() -> EditorOptions;
113
114        /// 语言(默认 'sql')。
115        #[wasm_bindgen(method, setter, js_name = language)]
116        pub fn set_language(this: &EditorOptions, v: &str);
117
118        /// 主题:'light'(Catppuccin Latte)或 'dark'(Catppuccin Mocha)。
119        #[wasm_bindgen(method, setter, js_name = theme)]
120        pub fn set_theme(this: &EditorOptions, v: &str);
121
122        /// 是否启用 Vim keymap。
123        #[wasm_bindgen(method, setter, js_name = vim)]
124        pub fn set_vim(this: &EditorOptions, v: bool);
125
126        /// SQL 补全 schema(表/列数据)。v 为 serde_wasm_bindgen::to_value 序列化结果。
127        #[wasm_bindgen(method, setter, js_name = schema)]
128        pub fn set_schema(this: &EditorOptions, v: &wasm_bindgen::JsValue);
129
130        /// 初始文档内容。
131        #[wasm_bindgen(method, setter, js_name = value)]
132        pub fn set_value(this: &EditorOptions, v: &str);
133
134        /// 文档变更回调(参数为最新全文)。
135        #[wasm_bindgen(method, setter, js_name = onChange)]
136        pub fn set_on_change(this: &EditorOptions, cb: &Closure<dyn FnMut(String)>);
137
138        /// 编辑器就绪回调(构造末尾同步触发一次)。
139        #[wasm_bindgen(method, setter, js_name = onReady)]
140        pub fn set_on_ready(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
141
142        /// Ctrl/Cmd + Enter 快捷键回调(SQL 控制台触发执行)。
143        #[wasm_bindgen(method, setter, js_name = onRunShortcut)]
144        pub fn set_on_run_shortcut(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
145    }
146
147    /// 编辑器实例句柄:持有 instance + 所有 Closure,Drop 时销毁实例并释放闭包。
148    ///
149    /// 闭包字段 `_` 前缀表示仅用于保持生命周期——它们被注入 JS 后,JS 侧持有
150    /// 函数引用;只要 [`EditorHandle`] 存活,闭包就不会被回收。Drop 时随结构释放。
151    pub struct EditorHandle {
152        instance: EditorInstance,
153        _on_change: Closure<dyn FnMut(String)>,
154        _on_ready: Closure<dyn FnMut()>,
155        _on_run_shortcut: Closure<dyn FnMut()>,
156    }
157
158    impl EditorHandle {
159        /// 调用方须先把各 closure set 进 EditorOptions,再 create,
160        /// 然后把返回的 instance + 同名 closure 一起传入 new。
161        /// `on_run_shortcut` 对应 Ctrl/Cmd+Enter 回调;不用该功能时传 no-op 闭包。
162        pub fn new(
163            instance: EditorInstance,
164            on_change: Closure<dyn FnMut(String)>,
165            on_ready: Closure<dyn FnMut()>,
166            on_run_shortcut: Closure<dyn FnMut()>,
167        ) -> Self {
168            Self {
169                instance,
170                _on_change: on_change,
171                _on_ready: on_ready,
172                _on_run_shortcut: on_run_shortcut,
173            }
174        }
175
176        /// 借用底层实例,供宿主调 getValue/setTheme/setSchema 等。
177        pub fn instance(&self) -> &EditorInstance {
178            &self.instance
179        }
180    }
181
182    impl Drop for EditorHandle {
183        fn drop(&mut self) {
184            // 销毁 JS 侧编辑器;随后 _on_change/_on_ready 字段按声明顺序释放,
185            // 释放 wasm-bindgen 函数表槽位。
186            self.instance.destroy();
187        }
188    }
189}
190
191#[cfg(target_arch = "wasm32")]
192pub use wasm::*;