fix(shortcuts): record shifted punctuation by its unshifted key

This commit is contained in:
Thanadon Kongkanun
2026-09-11 23:26:30 +08:00
committed by GitHub
parent 8b1379ff4f
commit 35773ace52
2 changed files with 109 additions and 4 deletions
@@ -17,6 +17,7 @@ import {
tabSwitcherDirectionFromShortcut,
} from "@/lib/editor/keyboardShortcuts";
import { formatShortcutDisplay, isMacShortcutPlatform } from "@/lib/editor/shortcutDisplay";
import { shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
describe("keyboard shortcut matching", () => {
it("records modifier-only mouse shortcut settings", () => {
@@ -44,6 +45,31 @@ describe("keyboard shortcut matching", () => {
expect(eventToShortcut({ key: "+", ctrlKey: true, shiftKey: true }, "Win32")).toBe("Shift+Mod+Plus");
});
it("records shifted punctuation by its unshifted key so CodeMirror can resolve the binding", () => {
// CodeMirror registers and looks up "Shift-" bindings against the physical
// key ("/"), never the shifted character ("?"), so recording "?" produced a
// binding that could never fire.
expect(eventToShortcut({ key: "?", code: "Slash", ctrlKey: true, shiftKey: true }, "Win32")).toBe("Shift+Mod+/");
expect(shortcutToCodeMirrorKey(eventToShortcut({ key: "?", code: "Slash", ctrlKey: true, shiftKey: true }, "Win32")!)).toBe("Shift-Mod-/");
expect(eventToShortcut({ key: "!", code: "Digit1", ctrlKey: true, shiftKey: true }, "Win32")).toBe("Shift+Mod+1");
expect(eventToShortcut({ key: ":", code: "Semicolon", altKey: true, shiftKey: true }, "Win32")).toBe("Shift+Alt+;");
});
it("matches a press against a shortcut stored with the unshifted key", () => {
const press = { key: "?", code: "Slash", ctrlKey: true, shiftKey: true };
expect(matchesShortcut(press, "Shift+Mod+/", "Win32")).toBe(true);
// The shifted character still matches, so shortcuts saved by older versions keep working.
expect(matchesShortcut(press, "Shift+Mod+?", "Win32")).toBe(true);
// A different physical key must not match.
expect(matchesShortcut({ key: "?", code: "Period", ctrlKey: true, shiftKey: true }, "Shift+Mod+/", "Win32")).toBe(false);
});
it("leaves unshifted and letter keys untouched", () => {
expect(eventToShortcut({ key: "/", code: "Slash", ctrlKey: true }, "Win32")).toBe("Mod+/");
expect(eventToShortcut({ key: "A", code: "KeyA", ctrlKey: true, shiftKey: true }, "Win32")).toBe("Shift+Mod+A");
});
it.each([
["¨", "KeyU", "Shift+Alt+U"],
["Ò", "KeyL", "Shift+Alt+L"],
@@ -89,13 +115,37 @@ describe("keyboard shortcut matching", () => {
const combinedShortcut = eventToShortcut({ key: "b", ctrlKey: true, metaKey: true, shiftKey: true, altKey: true }, "MacIntel");
expect(combinedShortcut).toBe("Shift+Ctrl+Mod+Alt+B");
expect(matchesShortcut({ key: "b", ctrlKey: true, metaKey: true, shiftKey: true, altKey: true }, combinedShortcut!, "MacIntel")).toBe(true);
expect(
matchesShortcut(
{
key: "b",
ctrlKey: true,
metaKey: true,
shiftKey: true,
altKey: true,
},
combinedShortcut!,
"MacIntel",
),
).toBe(true);
expect(matchesShortcut({ key: "b", ctrlKey: true, shiftKey: true, altKey: true }, combinedShortcut!, "MacIntel")).toBe(false);
expect(matchesShortcut({ key: "b", metaKey: true, shiftKey: true, altKey: true }, combinedShortcut!, "MacIntel")).toBe(false);
const nonMacCombinedShortcut = eventToShortcut({ key: "b", ctrlKey: true, metaKey: true, shiftKey: true, altKey: true }, "Win32");
expect(nonMacCombinedShortcut).toBe("Shift+Mod+Meta+Alt+B");
expect(matchesShortcut({ key: "b", ctrlKey: true, metaKey: true, shiftKey: true, altKey: true }, nonMacCombinedShortcut!, "Win32")).toBe(true);
expect(
matchesShortcut(
{
key: "b",
ctrlKey: true,
metaKey: true,
shiftKey: true,
altKey: true,
},
nonMacCombinedShortcut!,
"Win32",
),
).toBe(true);
expect(matchesShortcut({ key: "b", ctrlKey: true, shiftKey: true, altKey: true }, nonMacCombinedShortcut!, "Win32")).toBe(false);
expect(matchesShortcut({ key: "b", metaKey: true, shiftKey: true, altKey: true }, nonMacCombinedShortcut!, "Win32")).toBe(false);
});
@@ -109,7 +159,11 @@ describe("keyboard shortcut matching", () => {
const isMac = isMacShortcutPlatform();
const platformModEvent = isMac ? { key: "\\", metaKey: true } : { key: "\\", ctrlKey: true };
expect(isExecuteSqlInNewResultTabShortcut(platformModEvent, { executeSqlInNewResultTab: "Mod+\\" })).toBe(true);
expect(
isExecuteSqlInNewResultTabShortcut(platformModEvent, {
executeSqlInNewResultTab: "Mod+\\",
}),
).toBe(true);
expect(isExecuteSqlInNewResultTabShortcut({ ...platformModEvent, shiftKey: true }, { executeSqlInNewResultTab: "Mod+\\" })).toBe(false);
expect(isExecuteSqlInNewResultTabShortcut({ key: "\\", ctrlKey: true }, { executeSqlInNewResultTab: "Mod+\\" })).toBe(!isMac);
expect(isExecuteSqlInNewResultTabShortcut({ key: "\\", metaKey: true }, { executeSqlInNewResultTab: "Mod+\\" })).toBe(true);
@@ -118,7 +172,11 @@ describe("keyboard shortcut matching", () => {
it("matches the configurable Zen mode shortcut", () => {
const platformModEvent = isMacShortcutPlatform() ? { key: "F12", metaKey: true, shiftKey: true } : { key: "F12", ctrlKey: true, shiftKey: true };
expect(isToggleZenModeShortcut(platformModEvent, { toggleZenMode: "Shift+Mod+F12" })).toBe(true);
expect(
isToggleZenModeShortcut(platformModEvent, {
toggleZenMode: "Shift+Mod+F12",
}),
).toBe(true);
expect(isToggleZenModeShortcut({ ...platformModEvent, shiftKey: false }, { toggleZenMode: "Shift+Mod+F12" })).toBe(false);
expect(isToggleZenModeShortcut(platformModEvent, { toggleZenMode: "" })).toBe(false);
});
@@ -17,8 +17,51 @@ function normalizeKey(key: string): string {
return key.length === 1 ? key.toLowerCase() : key;
}
// Physical keys whose unshifted character CodeMirror always registers bindings
// under. `KeyboardEvent.key` reports the *shifted* character instead ("/" -> "?",
// "1" -> "!"), so a shortcut recorded from that character can never match the
// binding CodeMirror looks up. Letters are excluded: they already round-trip
// because `shortcutToCodeMirrorKey` lowercases them.
const BASE_KEY_BY_CODE: Record<string, string> = {
Digit0: "0",
Digit1: "1",
Digit2: "2",
Digit3: "3",
Digit4: "4",
Digit5: "5",
Digit6: "6",
Digit7: "7",
Digit8: "8",
Digit9: "9",
Backquote: "`",
Minus: "-",
Equal: "=",
BracketLeft: "[",
BracketRight: "]",
Backslash: "\\",
Semicolon: ";",
Quote: "'",
Comma: ",",
Period: ".",
Slash: "/",
};
/**
* The unshifted character for a shifted punctuation/digit press, or null when
* the event is not one. Mirrors CodeMirror's `base[event.keyCode]` table, which
* is what its keymap resolves `Shift-` bindings against.
*/
function baseKeyForShiftedEvent(event: ShortcutLikeEvent): string | null {
if (!event.shiftKey || event.key.length !== 1) return null;
const baseKey = BASE_KEY_BY_CODE[event.code ?? ""];
return baseKey && baseKey !== event.key ? baseKey : null;
}
function matchesShortcutKey(event: ShortcutLikeEvent, key: string, platform = globalThis.navigator?.platform || ""): boolean {
if (normalizeKey(event.key) === normalizeKey(key)) return true;
// Shortcuts store the unshifted character for shifted punctuation/digits (see
// baseKeyForShiftedEvent), so match those against the physical key too.
if (baseKeyForShiftedEvent(event) === normalizeKey(key)) return true;
// KeyboardEvent.code 回退:event.key 随布局/修饰键变形时按物理键位匹配
// (macOS Option+字母 → 变形字符如 ⌥W="∑";俄文等非拉丁布局 → "Ц")。
// 仅限 Alt 组合键场景
@@ -41,6 +84,10 @@ function shortcutKeyNameFromEvent(event: ShortcutLikeEvent, platform: string): s
if (event.altKey && isMacShortcutPlatform(platform) && /^Key[A-Z]$/.test(event.code ?? "")) {
return event.code!.slice(3);
}
// Record "Shift+Mod+/" rather than "Shift+Mod+?" so the CodeMirror binding
// ("Shift-Ctrl-/") is one the keymap actually resolves for this key press.
const baseKey = baseKeyForShiftedEvent(event);
if (baseKey) return shortcutKeyName(baseKey);
return shortcutKeyName(event.key);
}