diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 778bd9c5b7..16c4769500 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -103,6 +103,7 @@ import type { SqlHighlighter } from "@/lib/sql/sqlHighlighter"; import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, loadEditorTheme, editorFontTheme, sqlCompletionTheme, sqlSemanticHighlightTheme } from "@/lib/editor/editorThemes"; import { createStatementGutterMarkerDom, shouldShowStatementGutter } from "@/lib/editor/codemirrorStatementGutter"; import { createQueryEditorSearchKeymap } from "@/lib/editor/queryEditorSearchKeymap"; +import { searchKeymapWithoutModD } from "@/lib/editor/codemirrorSearchKeymap"; import { appendSqlCompletionSpace } from "@/lib/editor/sqlCompletionInsertion"; import { compareSqlCompletions, completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation"; import { clampEditorFontSize, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom"; @@ -4855,7 +4856,7 @@ onMounted(async () => { activeLineHighlighter, // Vim must be mounted before DBX/default keymaps so normal-mode keys are handled first. vimModeComp.of(vimModeExtension(initialSettings.vimModeEnabled)), - keymap.of([...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap]), + keymap.of([...defaultKeymap, ...searchKeymapWithoutModD(searchKeymap), ...historyKeymap, ...foldKeymap, ...completionKeymap]), sqlLanguageComp.of(buildSqlLanguageExtension()), sqlSemanticHighlightComp.of(buildSqlSemanticHighlightExtension()), tooltips({ parent: tooltipParent }), diff --git a/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue b/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue index 1cfbe8ff3d..385086bd4a 100644 --- a/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue +++ b/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue @@ -10,6 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useToast } from "@/composables/useToast"; import { copyToClipboard } from "@/lib/common/clipboard"; import { saveTextFile } from "@/lib/export/saveTextFile"; +import { searchKeymapWithoutModD } from "@/lib/editor/codemirrorSearchKeymap"; import { DEFAULT_SQL_FORMATTER_SETTINGS, SQL_FORMATTER_CONFIG_FORMATTER, @@ -361,7 +362,7 @@ function jsonEditorKeymapExtension(modules: CodeMirrorModules) { const { keymap } = modules.view; const commands = modules.commands; const search = modules.search; - return keymap.of([...search.searchKeymap, ...commands.historyKeymap, ...commands.defaultKeymap]); + return keymap.of([...searchKeymapWithoutModD(search.searchKeymap), ...commands.historyKeymap, ...commands.defaultKeymap]); } async function initJsonEditor() { diff --git a/apps/desktop/src/lib/__tests__/editor/codemirrorSearchKeymap.spec.ts b/apps/desktop/src/lib/__tests__/editor/codemirrorSearchKeymap.spec.ts new file mode 100644 index 0000000000..a3557d04cb --- /dev/null +++ b/apps/desktop/src/lib/__tests__/editor/codemirrorSearchKeymap.spec.ts @@ -0,0 +1,48 @@ +// @vitest-environment happy-dom + +import { EditorState } from "@codemirror/state"; +import { EditorView, keymap, runScopeHandlers } from "@codemirror/view"; +import { searchKeymap } from "@codemirror/search"; +import { describe, expect, it } from "vitest"; +import { searchKeymapWithoutModD } from "@/lib/editor/codemirrorSearchKeymap"; + +describe("searchKeymapWithoutModD", () => { + it("drops the built-in Mod-d binding from the search keymap", () => { + const filtered = searchKeymapWithoutModD(searchKeymap); + expect(filtered.find((binding) => binding.key === "Mod-d")).toBeUndefined(); + }); + + it("keeps the other search bindings intact", () => { + const filtered = searchKeymapWithoutModD(searchKeymap); + const keys = filtered.map((binding) => binding.key); + expect(keys).toContain("Mod-f"); + expect(keys).toContain("Mod-g"); + expect(keys).toContain("Mod-Alt-g"); + expect(keys).toContain("Mod-Shift-l"); + expect(keys).toContain("F3"); + }); + + it("baseline: the stock searchKeymap consumes Ctrl+D (the bug being fixed)", () => { + const view = new EditorView({ + parent: document.createElement("div"), + state: EditorState.create({ + extensions: [keymap.of([...searchKeymap])], + }), + }); + + expect(runScopeHandlers(view, new KeyboardEvent("keydown", { key: "d", ctrlKey: true }), "editor")).toBe(true); + view.destroy(); + }); + + it("no longer consumes Ctrl+D, letting the event bubble to the global shortcut handler", () => { + const view = new EditorView({ + parent: document.createElement("div"), + state: EditorState.create({ + extensions: [keymap.of([...searchKeymapWithoutModD(searchKeymap)])], + }), + }); + + expect(runScopeHandlers(view, new KeyboardEvent("keydown", { key: "d", ctrlKey: true }), "editor")).toBe(false); + view.destroy(); + }); +}); diff --git a/apps/desktop/src/lib/editor/codemirrorSearchKeymap.ts b/apps/desktop/src/lib/editor/codemirrorSearchKeymap.ts new file mode 100644 index 0000000000..19b2bc9802 --- /dev/null +++ b/apps/desktop/src/lib/editor/codemirrorSearchKeymap.ts @@ -0,0 +1,17 @@ +import type { KeyBinding } from "@codemirror/view"; + +/** + * `@codemirror/search` 的 `searchKeymap` 内置硬编码了 `Mod-d` = `selectNextOccurrence` + * (多光标选择下一个匹配词),与 DBX 的可配置快捷键体系冲突: + * + * - DBX 中 `Mod+D` 默认绑定「复制行 / 复制当前行」等用户可配置动作(见 shortcutRegistry.ts); + * 一旦用户把 `Mod+D` 配置给其他动作(例如把「新建查询」改为 Ctrl+D),searchKeymap 的 + * `Mod-d` 会在编辑器中抢先匹配并 `preventDefault`,事件冒泡到 window 时 + * `defaultPrevented=true`,App.vue 的全局 handleKeydown 直接返回,用户配置的快捷键永不触发。 + * + * 这里移除 `Mod-d` 绑定,保证用户可配置的快捷键优先于 CodeMirror 内置硬编码键位 + * (与 #4544 修复 Mod+F / Mod+H 冲突的思路一致)。 + */ +export function searchKeymapWithoutModD(searchKeymap: readonly KeyBinding[]): KeyBinding[] { + return searchKeymap.filter((binding) => binding.key !== "Mod-d"); +}