fix(editor): let global shortcuts override CodeMirror bindings

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-08-18 15:24:16 +08:00
1 parent 9051ad35b8
commit f0cc5aefcf
4 files changed
+110 -2

No files matched your search

@@ -0,0 +1,75 @@
// @vitest-environment happy-dom
import { defaultKeymap } from "@codemirror/commands";
import { EditorSelection, EditorState } from "@codemirror/state";
import { EditorView, keymap, runScopeHandlers, type KeyBinding } from "@codemirror/view";
import { describe, expect, it } from "vitest";
import { defaultKeymapForGlobalShortcuts } from "@/lib/editor/codemirrorDefaultKeymap";
import { matchesShortcut } from "@/lib/editor/keyboardShortcuts";
const platforms = [
{ name: "macOS", platform: "MacIntel", modifier: "Meta", event: { metaKey: true } },
{ name: "Windows", platform: "Win32", modifier: "Ctrl", event: { ctrlKey: true } },
{ name: "Linux", platform: "Linux x86_64", modifier: "Ctrl", event: { ctrlKey: true } },
] as const;
function platformIndentBindings(bindings: readonly KeyBinding[], modifier: "Meta" | "Ctrl"): KeyBinding[] {
return bindings.filter((binding) => binding.key === "Mod-[" || binding.key === "Mod-]").map((binding) => ({ ...binding, key: binding.key?.replace("Mod", modifier) }));
}
function createView(doc: string, bindings: readonly KeyBinding[], selection?: EditorSelection): EditorView {
return new EditorView({
parent: document.createElement("div"),
state: EditorState.create({
doc,
selection,
extensions: [keymap.of(bindings)],
}),
});
}
describe("defaultKeymapForGlobalShortcuts", () => {
it("removes only the bracket binding occupied by a global shortcut", () => {
const filtered = defaultKeymapForGlobalShortcuts(defaultKeymap, { toggleSidebar: "Mod+[" });
expect(filtered.find((binding) => binding.key === "Mod-[")).toBeUndefined();
expect(filtered.find((binding) => binding.key === "Mod-]")).toBeDefined();
expect(filtered.find((binding) => binding.key === "Mod-a")).toBeDefined();
});
for (const { name, platform, modifier, event } of platforms) {
for (const key of ["[", "]"] as const) {
it(`${name} lets a conflicting global Mod+${key} action run while the editor is focused`, () => {
const shortcut = `Mod+${key}`;
const bindings = platformIndentBindings(defaultKeymapForGlobalShortcuts(defaultKeymap, { toggleSidebar: shortcut }), modifier);
const view = createView(" select 1", bindings, EditorSelection.cursor(2));
const keyboardEvent = new KeyboardEvent("keydown", { key, ...event });
let globalActionRuns = 0;
if (!runScopeHandlers(view, keyboardEvent, "editor") && matchesShortcut(keyboardEvent, shortcut, platform)) globalActionRuns++;
expect(globalActionRuns).toBe(1);
expect(view.state.doc.toString()).toBe(" select 1");
view.destroy();
});
}
it(`${name} preserves single-line Mod+[ outdent without a global conflict`, () => {
const bindings = platformIndentBindings(defaultKeymapForGlobalShortcuts(defaultKeymap, { toggleSidebar: "Mod+B" }), modifier);
const view = createView(" select 1", bindings, EditorSelection.cursor(2));
expect(runScopeHandlers(view, new KeyboardEvent("keydown", { key: "[", ...event }), "editor")).toBe(true);
expect(view.state.doc.toString()).toBe("select 1");
view.destroy();
});
it(`${name} preserves multi-line Mod+] indent without a global conflict`, () => {
const doc = "select 1\nselect 2";
const bindings = platformIndentBindings(defaultKeymapForGlobalShortcuts(defaultKeymap, { toggleSidebar: "Mod+B" }), modifier);
const view = createView(doc, bindings, EditorSelection.range(0, doc.length));
expect(runScopeHandlers(view, new KeyboardEvent("keydown", { key: "]", ...event }), "editor")).toBe(true);
expect(view.state.doc.toString()).toBe(" select 1\n select 2");
view.destroy();
});
}
});
@@ -29,7 +29,8 @@ describe("QueryEditor block comment", () => {
expect(queryEditorSource).toContain("shortcut: shortcuts.toggleBlockComment");
expect(queryEditorSource).toContain("...binding(shortcuts.toggleBlockComment, (view) => {");
expect(queryEditorSource).toContain("!supportsQueryEditorBlockComments(props.databaseType)");
expect(queryEditorSource).toContain("defaultKeymap.filter((item) => item.run !== toggleBlockComment)");
expect(queryEditorSource).toContain("defaultKeymapForGlobalShortcuts(codeMirrorDefaultKeymap, settingsStore.editorSettings.shortcuts).filter((item) => item.run !== codeMirrorToggleBlockComment)");
expect(queryEditorSource).toContain("defaultKeymapComp.reconfigure(defaultKeymapExtension())");
expect(queryEditorSource).toContain("codeMirrorToggleBlockComment = toggleBlockComment;");
});
@@ -0,0 +1,12 @@
import type { KeyBinding } from "@codemirror/view";
import { normalizeShortcutSettings, SHORTCUT_DEFINITIONS, shortcutToCodeMirrorKey, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
const indentBracketKeys = new Set(["Mod-[", "Mod-]"]);
const globalShortcutDefinitions = SHORTCUT_DEFINITIONS.filter((definition) => definition.scope === "global");
export function defaultKeymapForGlobalShortcuts(defaultKeymap: readonly KeyBinding[], shortcuts?: Partial<ShortcutSettings>): KeyBinding[] {
const normalizedShortcuts = normalizeShortcutSettings(shortcuts);
const occupiedGlobalKeys = new Set(globalShortcutDefinitions.map((definition) => shortcutToCodeMirrorKey(normalizedShortcuts[definition.id])).filter(Boolean));
return defaultKeymap.filter((binding) => !binding.key || !indentBracketKeys.has(binding.key) || !occupiedGlobalKeys.has(binding.key));
}