Files
dbx/apps/desktop/src/lib/editor/keyboardShortcuts.ts
T

308 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { isMacShortcutPlatform, parseShortcutParts } from "@/lib/editor/shortcutDisplay";
import { normalizeShortcutSettings, type ShortcutActionId, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
export interface ShortcutLikeEvent {
key: string;
code?: string;
metaKey?: boolean;
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
isComposing?: boolean;
}
function normalizeKey(key: string): string {
if (key === " ") return "Space";
if (key === "+" || key === "Plus") return "Plus";
return key.length === 1 ? key.toLowerCase() : key;
}
function matchesShortcutKey(event: ShortcutLikeEvent, key: string, platform = globalThis.navigator?.platform || ""): boolean {
if (normalizeKey(event.key) === normalizeKey(key)) return true;
// KeyboardEvent.code 回退:event.key 随布局/修饰键变形时按物理键位匹配
// (macOS Option+字母 → 变形字符如 ⌥W="∑";俄文等非拉丁布局 → "Ц")。
// 仅限 Alt 组合键场景
if (!event.altKey || !/^Key[A-Z]$/.test(event.code ?? "") || !/^[A-Z]$/i.test(key)) return false;
// 非 macOS 的 Ctrl+Alt 是 AltGr 特征:用户可能在输入字符(如波兰语
// AltGr+W → "ł"),按 code 强制匹配会让全局快捷键在打字时误触发
if (!isMacShortcutPlatform(platform) && event.ctrlKey) return false;
return event.code!.slice(3).toLowerCase() === key.toLowerCase();
}
function shortcutKeyName(key: string): string | null {
if (key === " ") return "Space";
if (key === "+") return "Plus";
if (["Control", "Meta", "Shift", "Alt"].includes(key)) return null;
if (key.length === 1) return key.toUpperCase();
return key;
}
export function eventToShortcut(event: ShortcutLikeEvent, platform = globalThis.navigator?.platform || ""): 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 && event.key.length === 1 && event.key !== " ") return null;
const isMac = isMacShortcutPlatform(platform);
const parts: string[] = [];
if (event.shiftKey) parts.push("Shift");
if (isMac) {
if (event.ctrlKey) parts.push("Ctrl");
if (event.metaKey) parts.push("Mod");
} else if (event.ctrlKey && event.metaKey) {
parts.push("Mod", "Meta");
} else if (event.ctrlKey || event.metaKey) {
parts.push("Mod");
}
if (event.altKey) parts.push("Alt");
parts.push(key);
return parts.join("+");
}
export function eventToModifierOnlyShortcut(event: ShortcutLikeEvent, platform = globalThis.navigator?.platform || ""): string | null {
if (event.isComposing) return null;
if (event.key === "Alt") return "Alt";
if (event.key === "Shift") return "Shift";
if (event.key === "Meta") return isMacShortcutPlatform(platform) ? "Mod" : "Meta";
if (event.key === "Control") return isMacShortcutPlatform(platform) ? "Ctrl" : "Mod";
return null;
}
export function matchesModifierOnlyShortcut(event: Omit<ShortcutLikeEvent, "key">, shortcut: string): boolean {
if (event.isComposing || !shortcut) return false;
const meta = !!event.metaKey;
const ctrl = !!event.ctrlKey;
const alt = !!event.altKey;
const shift = !!event.shiftKey;
if (shortcut === "Mod") return meta !== ctrl && !alt && !shift;
if (shortcut === "Meta") return meta && !ctrl && !alt && !shift;
if (shortcut === "Ctrl") return ctrl && !meta && !alt && !shift;
if (shortcut === "Alt") return alt && !meta && !ctrl && !shift;
if (shortcut === "Shift") return shift && !meta && !ctrl && !alt;
return false;
}
export function matchesShortcut(event: ShortcutLikeEvent, shortcut: string, platform = globalThis.navigator?.platform || ""): boolean {
if (event.isComposing || !shortcut) return false;
const parts = parseShortcutParts(shortcut);
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 (isMacShortcutPlatform(platform)) {
if (!event.metaKey || !!event.ctrlKey !== usesCtrl) return false;
} else {
if (!event.metaKey && !event.ctrlKey) return false;
if (usesCtrl && (!event.ctrlKey || !event.metaKey)) return false;
if (usesMeta && (!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 matchesShortcutKey(event, key, platform);
}
function actionShortcut(actionId: ShortcutActionId, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): string {
return normalizeShortcutSettings(shortcuts, platform)[actionId];
}
const SWITCH_TO_TAB_ACTIONS: ShortcutActionId[] = ["switchToTab1", "switchToTab2", "switchToTab3", "switchToTab4", "switchToTab5", "switchToTab6", "switchToTab7", "switchToTab8", "switchToTab9"];
export function isExecuteSqlShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("executeSql", shortcuts));
}
export function isExecuteSqlInNewResultTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("executeSqlInNewResultTab", shortcuts));
}
export function isCloseTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("closeTab", shortcuts));
}
export function isCloseOtherTabsShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): boolean {
return matchesShortcut(event, actionShortcut("closeOtherTabs", shortcuts), platform);
}
export function isSendSelectionToAiShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("sendSelectionToAi", shortcuts));
}
export function isNewQueryShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("newQuery", shortcuts));
}
export function isOpenSettingsShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("openSettings", shortcuts));
}
export function isFocusSearchShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("focusSearch", shortcuts));
}
export function isRefreshDataShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("refreshData", shortcuts));
}
export function isToggleResultsPaneShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("toggleResultsPane", shortcuts));
}
export function isModRShortcut(event: ShortcutLikeEvent): boolean {
return matchesShortcut(event, "Mod+R");
}
export function isZoomInShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
if (matchesShortcut(event, actionShortcut("zoomInUi", shortcuts))) return true;
if (event.isComposing || event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return normalizeKey(event.key) === "NumpadAdd" && !event.shiftKey;
}
export function isZoomOutShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
if (matchesShortcut(event, actionShortcut("zoomOutUi", shortcuts))) return true;
if (event.isComposing || event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return normalizeKey(event.key) === "NumpadSubtract" && !event.shiftKey;
}
export function isResetZoomShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
if (matchesShortcut(event, actionShortcut("resetUiZoom", shortcuts))) return true;
if (event.isComposing || event.altKey || event.shiftKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return normalizeKey(event.key) === "Numpad0";
}
export function isToggleTransposeShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("toggleTranspose", shortcuts));
}
export function isSaveShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("saveSql", shortcuts));
}
export function isAcceptCompletionShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("acceptCompletion", shortcuts));
}
export function isObjectSourceSaveShortcutTarget(target: { closest(selector: string): unknown } | null | undefined): boolean {
return !!target?.closest("[data-object-source-editor], [data-object-source-preview]");
}
export function isCopyCurrentRowShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("copyCurrentRow", shortcuts));
}
export function isEditTableStructureShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): boolean {
return matchesShortcut(event, actionShortcut("editTableStructure", shortcuts, platform), platform);
}
export function isDeleteCurrentRowShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("deleteCurrentRow", shortcuts));
}
export function isGoToColumnShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): boolean {
return matchesShortcut(event, actionShortcut("goToColumn", shortcuts, platform), platform);
}
export function isGoToFirstPageShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("goToFirstPage", shortcuts));
}
export function isGoToPreviousPageShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("goToPreviousPage", shortcuts));
}
export function isGoToNextPageShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("goToNextPage", shortcuts));
}
export function isGoToLastPageShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("goToLastPage", shortcuts));
}
export function isCancelSearchShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("cancelSearch", shortcuts));
}
export function isToggleSidebarShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("toggleSidebar", shortcuts));
}
export function isCopySidebarSelectionShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("copySidebarSelection", shortcuts));
}
export function isPasteSidebarSelectionShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("pasteSidebarSelection", shortcuts));
}
export function isEditSidebarConnectionShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("editSidebarConnection", shortcuts));
}
export function isViewTableDdlShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("viewTableDdl", shortcuts));
}
export function isQuickOpenShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("quickOpen", shortcuts));
}
export function isNavigateTabHistoryBackShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): boolean {
return matchesShortcut(event, actionShortcut("navigateTabHistoryBack", shortcuts, platform), platform);
}
export function isNavigateTabHistoryForwardShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): boolean {
return matchesShortcut(event, actionShortcut("navigateTabHistoryForward", shortcuts, platform), platform);
}
export function handleTabHistoryNavigationShortcut(event: ShortcutLikeEvent, shortcuts: Partial<ShortcutSettings> | undefined, navigate: (direction: -1 | 1) => boolean, platform = globalThis.navigator?.platform || ""): boolean {
if (isNavigateTabHistoryBackShortcut(event, shortcuts, platform)) return navigate(-1);
if (isNavigateTabHistoryForwardShortcut(event, shortcuts, platform)) return navigate(1);
return false;
}
export function isSwitchToPreviousTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("switchToPreviousTab", shortcuts));
}
export function isSwitchToNextTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("switchToNextTab", shortcuts));
}
/**
* JetBrains-style tab switcher: exact match advances forward; adding Shift to
* the configured combo (Ctrl+Shift+Tab by default) moves backward. Returns
* null when the event is not the switcher shortcut.
*/
export function tabSwitcherDirectionFromShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): -1 | 1 | null {
const shortcut = actionShortcut("tabSwitcher", shortcuts, platform);
if (matchesShortcut(event, shortcut, platform)) return 1;
if (event.shiftKey && matchesShortcut({ ...event, shiftKey: false }, shortcut, platform)) return -1;
return null;
}
export function switchToTabIndexFromShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): number | null {
const normalized = normalizeShortcutSettings(shortcuts);
const index = SWITCH_TO_TAB_ACTIONS.findIndex((actionId) => matchesShortcut(event, normalized[actionId]));
return index >= 0 ? index : null;
}
export function isBrowserReloadShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing || event.altKey) return false;
const key = normalizeKey(event.key);
if (key === "F5") return true;
return key === "r" && (!!event.metaKey || !!event.ctrlKey);
}