feat(tabs): add tab navigation history shortcuts

This commit is contained in:
funnytime authored and GitHub committed 2026-08-20 09:28:39 +08:00
1 parent c7797b4be1
commit 8213da233b
18 files changed
+406 -17

No files matched your search

@@ -112,8 +112,8 @@ export function matchesShortcut(event: ShortcutLikeEvent, shortcut: string, plat
return matchesShortcutKey(event, key, platform);
}
function actionShortcut(actionId: ShortcutActionId, shortcuts?: Partial<ShortcutSettings>): string {
return normalizeShortcutSettings(shortcuts)[actionId];
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"];
@@ -231,6 +231,20 @@ export function isQuickOpenShortcut(event: ShortcutLikeEvent, shortcuts?: Partia
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));
}
+67 -11
View File
@@ -36,6 +36,8 @@ export type ShortcutActionId =
| "closeOtherTabs"
| "focusSearch"
| "quickOpen"
| "navigateTabHistoryBack"
| "navigateTabHistoryForward"
| "switchToPreviousTab"
| "switchToNextTab"
| "switchToTab1"
@@ -87,8 +89,19 @@ export function closeOtherTabsDefaultShortcut(platform = globalThis.navigator?.p
return isMacShortcutPlatform(platform) ? "Alt+Mod+W" : "Shift+Alt+W";
}
const CLOSE_OTHER_TABS_PLATFORM_DEFAULTS = new Set(["Alt+Mod+W", "Shift+Alt+W"]);
export function tabNavigationHistoryDefaultShortcut(direction: "back" | "forward", platform = globalThis.navigator?.platform || ""): string {
const modifier = isMacShortcutPlatform(platform) ? "Ctrl" : "Mod";
const key = direction === "back" ? "ArrowLeft" : "ArrowRight";
return `${modifier}+Alt+${key}`;
}
const PLATFORM_DEFAULT_SHORTCUTS: Partial<Record<ShortcutActionId, ReadonlySet<string>>> = {
closeOtherTabs: new Set(["Alt+Mod+W", "Shift+Alt+W"]),
navigateTabHistoryBack: new Set(["Ctrl+Alt+ArrowLeft", "Mod+Alt+ArrowLeft"]),
navigateTabHistoryForward: new Set(["Ctrl+Alt+ArrowRight", "Mod+Alt+ArrowRight"]),
};
const LEGACY_CLOSE_TAB_DEFAULT = "Meta+W";
const TAB_NAVIGATION_HISTORY_ACTIONS: ShortcutActionId[] = ["navigateTabHistoryBack", "navigateTabHistoryForward"];
export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
{
@@ -301,6 +314,18 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
scope: "global",
defaultShortcut: "Mod+P",
},
{
id: "navigateTabHistoryBack",
labelKey: "settings.shortcutNavigateTabHistoryBack",
scope: "global",
defaultShortcut: tabNavigationHistoryDefaultShortcut("back"),
},
{
id: "navigateTabHistoryForward",
labelKey: "settings.shortcutNavigateTabHistoryForward",
scope: "global",
defaultShortcut: tabNavigationHistoryDefaultShortcut("forward"),
},
{
id: "switchToPreviousTab",
labelKey: "settings.shortcutSwitchToPreviousTab",
@@ -476,16 +501,36 @@ export function normalizeModifierOnlyShortcut(shortcut: string, fallback = ""):
return modifierOnlyShortcuts.has(normalized) ? normalized : fallback;
}
export function normalizeShortcutSettings(settings?: Partial<ShortcutSettings>): ShortcutSettings {
return Object.fromEntries(
function hasExplicitShortcut(settings: Partial<ShortcutSettings> | undefined, actionId: ShortcutActionId): boolean {
return !!settings && Object.prototype.hasOwnProperty.call(settings, actionId) && typeof settings[actionId] === "string";
}
function shortcutsUseSameKeys(first: string, second: string, platform = globalThis.navigator?.platform || ""): boolean {
return !!first && !!second && formatShortcut(first, platform).toLowerCase() === formatShortcut(second, platform).toLowerCase();
}
function shortcutDefaultForPlatform(definition: ShortcutDefinition, platform: string): string {
if (definition.id === "closeOtherTabs") return closeOtherTabsDefaultShortcut(platform);
if (definition.id === "navigateTabHistoryBack") return tabNavigationHistoryDefaultShortcut("back", platform);
if (definition.id === "navigateTabHistoryForward") return tabNavigationHistoryDefaultShortcut("forward", platform);
return definition.defaultShortcut;
}
export function needsTabNavigationHistoryShortcutMigration(settings?: Partial<ShortcutSettings>): boolean {
return !!settings && TAB_NAVIGATION_HISTORY_ACTIONS.some((actionId) => !hasExplicitShortcut(settings, actionId));
}
export function normalizeShortcutSettings(settings?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): ShortcutSettings {
const normalized = Object.fromEntries(
SHORTCUT_DEFINITIONS.map((definition) => {
const configuredValue = settings?.[definition.id];
let configured = typeof configuredValue === "string" ? configuredValue : definition.defaultShortcut;
// 云同步会把另一平台的默认值当作显式配置带过来(macOS 的 Alt+Mod+W 到
// Windows 上会还原成 Ctrl+Alt+W)。凡是平台默认集合内的值都视为"未
// 自定义",按本机平台重新解析;用户真正自定义的其他组合原样保留
if (definition.id === "closeOtherTabs" && CLOSE_OTHER_TABS_PLATFORM_DEFAULTS.has(configured)) {
configured = definition.defaultShortcut;
const platformDefault = shortcutDefaultForPlatform(definition, platform);
let configured = typeof configuredValue === "string" ? configuredValue : platformDefault;
// 云同步会把另一平台的默认值当作显式配置带过来。平台默认集合内的值视为
// 未自定义,按本机平台重新解析;用户真正自定义的其他组合原样保留
const platformDefaults = PLATFORM_DEFAULT_SHORTCUTS[definition.id];
if (platformDefaults?.has(configured)) {
configured = platformDefault;
}
// Meta+W was the old macOS-only default. Treat that exact value as a
// legacy default so existing Windows/Linux settings adopt Ctrl+W.
@@ -496,6 +541,17 @@ export function normalizeShortcutSettings(settings?: Partial<ShortcutSettings>):
return [definition.id, normalized];
}),
) as ShortcutSettings;
for (const actionId of TAB_NAVIGATION_HISTORY_ACTIONS) {
if (hasExplicitShortcut(settings, actionId)) continue;
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
if (!definition) continue;
const defaultShortcut = normalized[actionId];
const occupiedByExistingAction = SHORTCUT_DEFINITIONS.some((item) => item.id !== actionId && item.scope === definition.scope && hasExplicitShortcut(settings, item.id) && shortcutsUseSameKeys(normalized[item.id], defaultShortcut, platform));
if (occupiedByExistingAction) normalized[actionId] = "";
}
return normalized;
}
export function shortcutToCodeMirrorKey(shortcut: string): string {
@@ -521,11 +577,11 @@ export function formatShortcut(shortcut: string, platform = globalThis.navigator
.join("+");
}
export function findShortcutConflict(actionId: ShortcutActionId, shortcut: string, shortcuts: ShortcutSettings): ShortcutActionId | null {
export function findShortcutConflict(actionId: ShortcutActionId, shortcut: string, shortcuts: ShortcutSettings, platform = globalThis.navigator?.platform || ""): ShortcutActionId | null {
if (!shortcut) return 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);
const conflict = SHORTCUT_DEFINITIONS.find((item) => item.id !== actionId && item.scope === definition.scope && shortcutsUseSameKeys(shortcuts[item.id], shortcut, platform));
return conflict?.id ?? null;
}
@@ -0,0 +1,30 @@
export interface TabNavigationHistory {
entries: string[];
index: number;
}
export interface TabNavigationHistoryMove {
history: TabNavigationHistory;
tabId: string;
}
const MAX_TAB_NAVIGATION_HISTORY = 100;
export function createTabNavigationHistory(): TabNavigationHistory {
return { entries: [], index: -1 };
}
export function recordTabVisit(history: TabNavigationHistory, tabId: string): TabNavigationHistory {
if (!tabId || history.entries[history.index] === tabId) return history;
const entries = [...history.entries.slice(0, history.index + 1), tabId].slice(-MAX_TAB_NAVIGATION_HISTORY);
return { entries, index: entries.length - 1 };
}
export function moveInTabNavigationHistory(history: TabNavigationHistory, direction: -1 | 1, openTabIds: ReadonlySet<string>, currentTabId: string | null = null): TabNavigationHistoryMove | null {
for (let index = history.index + direction; index >= 0 && index < history.entries.length; index += direction) {
const tabId = history.entries[index];
if (tabId !== currentTabId && openTabIds.has(tabId)) return { history: { entries: history.entries, index }, tabId };
}
return null;
}