feat(settings): add custom shortcut settings

This commit is contained in:
t8y2
2026-05-15 17:53:51 +08:00
parent 8d73e6e00d
commit 6db2430994
12 changed files with 417 additions and 78 deletions
+71 -21
View File
@@ -1,36 +1,87 @@
import {
DEFAULT_SHORTCUT_SETTINGS,
normalizeShortcutSettings,
type ShortcutActionId,
type ShortcutSettings,
} from "@/lib/shortcutRegistry";
export interface ShortcutLikeEvent {
key: string;
metaKey?: boolean;
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
isComposing?: boolean;
}
export function isExecuteSqlShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing) return false;
if (event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return event.key === "Enter";
function normalizeKey(key: string): string {
if (key === " ") return "Space";
return key.length === 1 ? key.toLowerCase() : key;
}
export function isCloseTabShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing) return false;
if (!event.metaKey) return false;
return event.key.toLowerCase() === "w";
function shortcutKeyName(key: string): string | null {
if (key === " ") return "Space";
if (["Control", "Meta", "Shift", "Alt"].includes(key)) return null;
if (key.length === 1) return key.toUpperCase();
return key;
}
export function isFocusSearchShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing) return false;
if (event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return event.key.toLowerCase() === "f";
export function eventToShortcut(event: ShortcutLikeEvent): string | null {
if (event.isComposing) return null;
const key = shortcutKeyName(event.key);
if (!key) return null;
const hasModifier = !!event.metaKey || !!event.ctrlKey || !!event.altKey || !!event.shiftKey;
if (!hasModifier && key.length === 1) return null;
const parts: string[] = [];
if (event.shiftKey) parts.push("Shift");
if (event.metaKey || event.ctrlKey) parts.push("Mod");
if (event.altKey) parts.push("Alt");
parts.push(key);
return parts.join("+");
}
export function isSaveShortcut(event: ShortcutLikeEvent): boolean {
export function matchesShortcut(event: ShortcutLikeEvent, shortcut: string): boolean {
if (event.isComposing) return false;
if (event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return event.key.toLowerCase() === "s";
const parts = shortcut.split("+");
const key = parts[parts.length - 1] ?? "";
const modifiers = new Set(parts.slice(0, -1));
const usesMod = modifiers.has("Mod");
const usesMeta = modifiers.has("Meta");
const usesCtrl = modifiers.has("Ctrl");
if (usesMod) {
if (!event.metaKey && !event.ctrlKey) return false;
} else {
if (!!event.metaKey !== usesMeta) return false;
if (!!event.ctrlKey !== usesCtrl) return false;
}
if (!!event.altKey !== modifiers.has("Alt")) return false;
if (!!event.shiftKey !== modifiers.has("Shift")) return false;
return normalizeKey(event.key) === normalizeKey(key);
}
function actionShortcut(actionId: ShortcutActionId, shortcuts?: Partial<ShortcutSettings>): string {
return normalizeShortcutSettings(shortcuts)[actionId] || DEFAULT_SHORTCUT_SETTINGS[actionId];
}
export function isExecuteSqlShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("executeSql", shortcuts));
}
export function isCloseTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("closeTab", shortcuts));
}
export function isFocusSearchShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("focusSearch", shortcuts));
}
export function isSaveShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("saveSql", shortcuts));
}
export function isObjectSourceSaveShortcutTarget(
@@ -39,7 +90,6 @@ export function isObjectSourceSaveShortcutTarget(
return !!target?.closest("[data-object-source-editor], [data-object-source-preview]");
}
export function isCancelSearchShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing) return false;
return event.key === "Escape";
export function isCancelSearchShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("cancelSearch", shortcuts));
}
+93
View File
@@ -0,0 +1,93 @@
export type ShortcutActionId = "executeSql" | "saveSql" | "closeTab" | "focusSearch" | "cancelSearch";
export type ShortcutScope = "global" | "editor" | "search";
export interface ShortcutDefinition {
id: ShortcutActionId;
labelKey: string;
scope: ShortcutScope;
defaultShortcut: string;
}
export type ShortcutSettings = Record<ShortcutActionId, string>;
export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
{
id: "executeSql",
labelKey: "settings.shortcutExecuteSql",
scope: "editor",
defaultShortcut: "Mod+Enter",
},
{
id: "saveSql",
labelKey: "settings.shortcutSaveSql",
scope: "editor",
defaultShortcut: "Mod+S",
},
{
id: "closeTab",
labelKey: "settings.shortcutCloseTab",
scope: "global",
defaultShortcut: "Meta+W",
},
{
id: "focusSearch",
labelKey: "settings.shortcutFocusSearch",
scope: "global",
defaultShortcut: "Mod+F",
},
{
id: "cancelSearch",
labelKey: "settings.shortcutCancelSearch",
scope: "search",
defaultShortcut: "Escape",
},
];
export const DEFAULT_SHORTCUT_SETTINGS: ShortcutSettings = Object.fromEntries(
SHORTCUT_DEFINITIONS.map((definition) => [definition.id, definition.defaultShortcut]),
) as ShortcutSettings;
export function normalizeShortcutSettings(settings?: Partial<ShortcutSettings>): ShortcutSettings {
return Object.fromEntries(
SHORTCUT_DEFINITIONS.map((definition) => [
definition.id,
typeof settings?.[definition.id] === "string" && settings[definition.id]?.trim()
? settings[definition.id]
: definition.defaultShortcut,
]),
) as ShortcutSettings;
}
export function shortcutToCodeMirrorKey(shortcut: string): string {
return shortcut
.split("+")
.map((part) => (part.length === 1 ? part.toLowerCase() : part))
.join("-");
}
export function formatShortcut(shortcut: string, platform = globalThis.navigator?.platform || ""): string {
const isMac = platform.toLowerCase().includes("mac");
return shortcut
.split("+")
.map((part) => {
if (part === "Mod") return isMac ? "Cmd" : "Ctrl";
if (part === "Meta") return isMac ? "Cmd" : "Meta";
return part;
})
.join("+");
}
export function findShortcutConflict(
actionId: ShortcutActionId,
shortcut: string,
shortcuts: ShortcutSettings,
): ShortcutActionId | null {
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
if (!definition) return null;
const conflict = SHORTCUT_DEFINITIONS.find(
(item) => item.id !== actionId && item.scope === definition.scope && shortcuts[item.id] === shortcut,
);
return conflict?.id ?? null;
}