From cda5a9223088748890c8bc43d79f469e5bf61da0 Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 8 Jul 2026 14:37:52 +0800 Subject: [PATCH] feat(runner): enable Vim mode toggle for CodeRunner sandbox --- libs/codemirror-editor/src/editor.ts | 7 ++++ src/codemirror_bridge.rs | 4 +++ src/components/code_runner/runner.rs | 51 +++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/libs/codemirror-editor/src/editor.ts b/libs/codemirror-editor/src/editor.ts index feb4a44..92a8435 100644 --- a/libs/codemirror-editor/src/editor.ts +++ b/libs/codemirror-editor/src/editor.ts @@ -204,6 +204,13 @@ export class CodeMirrorInstance { }); } + /** 热切换 Vim 模式,不重建实例(Compartment.reconfigure)。 */ + setVim(v: boolean): void { + this.view.dispatch({ + effects: this.vimCompartment.reconfigure(v ? [vim()] : []), + }); + } + /** 热切换语言(python/node/javascript/sql),不重建实例(Compartment.reconfigure)。 */ setLanguage(lang: string): void { this.language = lang; diff --git a/src/codemirror_bridge.rs b/src/codemirror_bridge.rs index 9562b0a..c4d495d 100644 --- a/src/codemirror_bridge.rs +++ b/src/codemirror_bridge.rs @@ -89,6 +89,10 @@ pub mod wasm { #[wasm_bindgen(method, js_name = setTheme)] pub fn set_theme(this: &EditorInstance, theme: &str); + /// 热切换 Vim 模式(Compartment.reconfigure,不重建实例)。 + #[wasm_bindgen(method, js_name = setVim)] + pub fn set_vim(this: &EditorInstance, v: bool); + /// 热切换语言(python/node/javascript/sql,Compartment.reconfigure)。 /// 由 CodeRunner 组件在挂载时按 data-lang 调用。 #[wasm_bindgen(method, js_name = setLanguage)] diff --git a/src/components/code_runner/runner.rs b/src/components/code_runner/runner.rs index f37a773..90818fb 100644 --- a/src/components/code_runner/runner.rs +++ b/src/components/code_runner/runner.rs @@ -66,6 +66,34 @@ pub fn CodeRunner( // → 骨架屏从 DOM 移除,露出真实编辑器。 let mut editor_ready = use_signal(|| false); + // Vim 模式状态(通过 localStorage 持久化偏好) + let mut vim_enabled = use_signal(|| { + #[cfg(target_arch = "wasm32")] + { + if let Some(window) = web_sys::window() { + if let Ok(Some(storage)) = window.local_storage() { + if let Ok(Some(val)) = storage.get_item("yggdrasil-code-runner-vim") { + return val == "true"; + } + } + } + } + false + }); + + let toggle_vim = move |_| { + let next = !vim_enabled(); + vim_enabled.set(next); + #[cfg(target_arch = "wasm32")] + { + if let Some(window) = web_sys::window() { + if let Ok(Some(storage)) = window.local_storage() { + let _ = storage.set_item("yggdrasil-code-runner-vim", &next.to_string()); + } + } + } + }; + // —— CodeMirror 挂载(仅 WASM)—— // 范式镜像 src/pages/admin/system.rs 的 SQL 控制台与 src/pages/admin/write.rs 的 Tiptap。 #[cfg(target_arch = "wasm32")] @@ -106,6 +134,7 @@ pub fn CodeRunner( let opts = codemirror_bridge::EditorOptions::new(); opts.set_language(&mount_language); opts.set_theme(theme_name); + opts.set_vim(*vim_enabled.read()); opts.set_value(&source_signal.read()); opts.set_on_change(&on_change); opts.set_on_ready(&on_ready); @@ -138,6 +167,14 @@ pub fn CodeRunner( } }); + // 监听 vim 模式切换并同步。 + use_effect(move || { + let enabled = vim_enabled(); + if let Some(h) = editor_handle.read().as_ref() { + h.instance().set_vim(enabled); + } + }); + // source prop 外部变更(如 admin 页面切换语言重置示例代码)同步到 // signal + 编辑器。通过 source_prop_signal 订阅其变化。 // 此 effect 仅读取 source_prop_signal,不读取 source_signal,避免用户编辑时误触重置回环。 @@ -241,9 +278,21 @@ pub fn CodeRunner( div { class: "rounded-2xl overflow-hidden border border-[var(--color-paper-border)] bg-[var(--color-paper-entry)]", // 顶栏:语言标签 + 运行按钮 div { class: "flex justify-between items-center px-4 py-2.5 border-b border-[var(--color-paper-border)] bg-[var(--color-paper-theme)]", - div { class: "flex items-center gap-2", + div { class: "flex items-center gap-3", span { class: "w-2 h-2 rounded-full bg-[var(--color-paper-accent)]" } span { class: "font-mono text-sm font-semibold text-[var(--color-paper-primary)]", "{language}" } + button { + class: format!( + "text-[10px] px-1.5 py-0.5 rounded border transition cursor-pointer {}", + if vim_enabled() { + "bg-[var(--color-paper-accent)]/15 text-[var(--color-paper-accent)] border-[var(--color-paper-accent)]/30 font-semibold" + } else { + "bg-transparent text-[var(--color-paper-tertiary)] border-[var(--color-paper-border)] hover:text-[var(--color-paper-primary)]" + } + ), + onclick: toggle_vim, + "Vim" + } } button { class: "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium rounded-full text-[var(--color-paper-theme)] bg-[var(--color-paper-accent)] hover:brightness-110 active:scale-[0.98] transition disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer",