feat(tabs): add tab navigation history shortcuts
This commit is contained in:
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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user