From 4239602f3d4bb1f78d6ed24a608c415d71dd5333 Mon Sep 17 00:00:00 2001 From: xfy Date: Sun, 5 Jul 2026 21:31:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(codemirror):=20=E6=94=AF=E6=8C=81=20Ctrl/C?= =?UTF-8?q?md+Enter=20=E8=BF=90=E8=A1=8C=E5=BF=AB=E6=8D=B7=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按钮一直写着「执行 (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 改动中完成 --- .../src/__tests__/editor.test.ts | 8 +++++++ libs/codemirror-editor/src/editor.ts | 24 +++++++++++++++++-- src/codemirror_bridge.rs | 8 +++++++ src/components/code_runner/runner.rs | 12 ++++++++-- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/libs/codemirror-editor/src/__tests__/editor.test.ts b/libs/codemirror-editor/src/__tests__/editor.test.ts index e744aeb..397a563 100644 --- a/libs/codemirror-editor/src/__tests__/editor.test.ts +++ b/libs/codemirror-editor/src/__tests__/editor.test.ts @@ -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 扩展加载不抛错 + }); }); diff --git a/libs/codemirror-editor/src/editor.ts b/libs/codemirror-editor/src/editor.ts index fda11f8..c0e06d3 100644 --- a/libs/codemirror-editor/src/editor.ts +++ b/libs/codemirror-editor/src/editor.ts @@ -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)), diff --git a/src/codemirror_bridge.rs b/src/codemirror_bridge.rs index d6d0963..9562b0a 100644 --- a/src/codemirror_bridge.rs +++ b/src/codemirror_bridge.rs @@ -147,6 +147,10 @@ pub mod wasm { /// 编辑器就绪回调(构造末尾同步触发一次)。 #[wasm_bindgen(method, setter, js_name = onReady)] pub fn set_on_ready(this: &EditorOptions, cb: &Closure); + + /// Ctrl/Cmd + Enter 快捷键回调(SQL 控制台触发执行)。 + #[wasm_bindgen(method, setter, js_name = onRunShortcut)] + pub fn set_on_run_shortcut(this: &EditorOptions, cb: &Closure); } /// 编辑器实例句柄:持有 instance + 所有 Closure,Drop 时销毁实例并释放闭包。 @@ -157,20 +161,24 @@ pub mod wasm { instance: EditorInstance, _on_change: Closure, _on_ready: Closure, + _on_run_shortcut: Closure, } 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, on_ready: Closure, + on_run_shortcut: Closure, ) -> Self { Self { instance, _on_change: on_change, _on_ready: on_ready, + _on_run_shortcut: on_run_shortcut, } } diff --git a/src/components/code_runner/runner.rs b/src/components/code_runner/runner.rs index 58fb7eb..363e40c 100644 --- a/src/components/code_runner/runner.rs +++ b/src/components/code_runner/runner.rs @@ -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)); } });