mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 08:13:34 +00:00
fix(shortcuts): record shifted punctuation by its unshifted key
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user