feat(runner): enable Vim mode toggle for CodeRunner sandbox

This commit is contained in:
xfy 2026-07-08 14:37:52 +08:00
parent 3bb67f6673
commit cda5a92230
3 changed files with 61 additions and 1 deletions

View File

@ -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;

View File

@ -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/sqlCompartment.reconfigure
/// 由 CodeRunner 组件在挂载时按 data-lang 调用。
#[wasm_bindgen(method, js_name = setLanguage)]

View File

@ -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",