feat(codemirror): 支持 Ctrl/Cmd+Enter 运行快捷键

按钮一直写着「执行 (Ctrl+Enter)」但快捷键从未接线——basicSetup 不含
自定义 keymap。从底层往上打通 Mod-Enter 运行通道:

editor.ts:
- EditorOptions 加 onRunShortcut 可选回调
- constructor 条件注入 keymap.of([{ key: "Mod-Enter", ... }])
- 用 Prec.highest 包裹:@replit/codemirror-vim 的 vimPlugin 在
  ViewPlugin 层拦截按键,普通 keymap 会被吞;vim 自己也用
  Prec.highest,跟随此惯例保证 Mod-Enter 在 vim 之前命中
- 放在 vimCompartment 之前,满足「vim 必须在 keymap 最前」约束
- Prec 从 @codemirror/state 引入(不在 @codemirror/view)

codemirror_bridge.rs:
- EditorOptions extern 加 set_on_run_shortcut
- EditorHandle 加 _on_run_shortcut 字段保生命周期,new() 签名增加
  该参数(Drop 随实例释放)

调用点适配:
- CodeRunner(runner.rs)传 no-op 闭包(不用该功能但签名要满足)
- SQL 控制台的接线在后续 system.rs 改动中完成
This commit is contained in:
xfy 2026-07-05 21:31:03 +08:00
parent 1dc02561a3
commit 4239602f3d
4 changed files with 48 additions and 4 deletions

View File

@ -64,4 +64,12 @@ describe('CodeMirrorInstance', () => {
expect(captured).toBe('hello');
inst.destroy();
});
it('onRunShortcut 注入则构造不抛错', () => {
const opts = new EditorOptions();
opts.onRunShortcut = () => {};
const inst = new CodeMirrorInstance(container, opts);
inst.destroy();
// happy-dom 无法触发真实按键,仅验证 keymap 扩展加载不抛错
});
});

View File

@ -1,8 +1,8 @@
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { PostgreSQL, sql } from '@codemirror/lang-sql';
import { Compartment, EditorState, type Extension } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { Compartment, EditorState, type Extension, Prec } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { vim } from '@replit/codemirror-vim';
import { basicSetup } from 'codemirror';
import { type ThemeName, themeExtension } from './themes';
@ -25,6 +25,8 @@ export class EditorOptions {
value?: string;
onChange?: (value: string) => void;
onReady?: () => void;
/** Ctrl/Cmd + Enter 快捷键回调SQL 控制台触发执行)。 */
onRunShortcut?: () => void;
}
/**
@ -75,6 +77,24 @@ export class CodeMirrorInstance {
doc: options.value ?? '',
extensions: [
basicSetup,
// Ctrl/Cmd + Enter 运行快捷键:用 Prec.highest 包裹,保证在 vim 的
// ViewPlugin 按键拦截之前命中vim 自己也用 Prec.highest跟随此惯例
...(options.onRunShortcut
? [
Prec.highest(
keymap.of([
{
key: 'Mod-Enter',
preventDefault: true,
run: () => {
options.onRunShortcut?.();
return true;
},
},
]),
),
]
: []),
// vim 必须在 keymap 最前(@replit/codemirror-vim 仓库要求)
this.vimCompartment.of(options.vim ? [vim()] : []),
this.themeCompartment.of(themeExtension(theme)),

View File

@ -147,6 +147,10 @@ pub mod wasm {
/// 编辑器就绪回调(构造末尾同步触发一次)。
#[wasm_bindgen(method, setter, js_name = onReady)]
pub fn set_on_ready(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
/// Ctrl/Cmd + Enter 快捷键回调SQL 控制台触发执行)。
#[wasm_bindgen(method, setter, js_name = onRunShortcut)]
pub fn set_on_run_shortcut(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
}
/// 编辑器实例句柄:持有 instance + 所有 ClosureDrop 时销毁实例并释放闭包。
@ -157,20 +161,24 @@ pub mod wasm {
instance: EditorInstance,
_on_change: Closure<dyn FnMut(String)>,
_on_ready: Closure<dyn FnMut()>,
_on_run_shortcut: Closure<dyn FnMut()>,
}
impl EditorHandle {
/// 调用方须先把各 closure set 进 EditorOptions再 create
/// 然后把返回的 instance + 同名 closure 一起传入 new。
/// `on_run_shortcut` 对应 Ctrl/Cmd+Enter 回调;不用该功能时传 no-op 闭包。
pub fn new(
instance: EditorInstance,
on_change: Closure<dyn FnMut(String)>,
on_ready: Closure<dyn FnMut()>,
on_run_shortcut: Closure<dyn FnMut()>,
) -> Self {
Self {
instance,
_on_change: on_change,
_on_ready: on_ready,
_on_run_shortcut: on_run_shortcut,
}
}

View File

@ -73,6 +73,9 @@ pub fn CodeRunner(
move |v: String| sig.set(v)
});
let on_ready = Closure::new(|| {});
// CodeRunner 不使用 Ctrl+Enter 运行快捷键(它有自己的运行按钮),
// 但 EditorHandle 签名要求该闭包,传 no-op 满足生命周期。
let on_run_shortcut = Closure::new(|| {});
let resolved = use_resolved_theme();
let theme_name = if resolved() == ResolvedTheme::Dark {
@ -87,12 +90,17 @@ pub fn CodeRunner(
opts.set_value(&source_signal.read());
opts.set_on_change(&on_change);
opts.set_on_ready(&on_ready);
opts.set_on_run_shortcut(&on_run_shortcut);
if let Ok(Some(inst)) =
codemirror_bridge::get_module().create(&mount_container_id, &opts)
{
let handle =
codemirror_bridge::EditorHandle::new(inst, on_change, on_ready);
let handle = codemirror_bridge::EditorHandle::new(
inst,
on_change,
on_ready,
on_run_shortcut,
);
editor_handle.set(Some(handle));
}
});