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

+32
View File
@@ -72,6 +72,7 @@ import {
isExecuteSqlShortcut, isExecuteSqlShortcut,
isFocusSearchShortcut, isFocusSearchShortcut,
isModRShortcut, isModRShortcut,
handleTabHistoryNavigationShortcut,
isNewQueryShortcut, isNewQueryShortcut,
isObjectSourceSaveShortcutTarget, isObjectSourceSaveShortcutTarget,
isOpenSettingsShortcut, isOpenSettingsShortcut,
@@ -88,6 +89,7 @@ import {
switchToTabIndexFromShortcut, switchToTabIndexFromShortcut,
} from "@/lib/editor/keyboardShortcuts"; } from "@/lib/editor/keyboardShortcuts";
import { isPreviewTab } from "@/lib/tabs/tabPresentation"; import { isPreviewTab } from "@/lib/tabs/tabPresentation";
import { createTabNavigationHistory, moveInTabNavigationHistory, recordTabVisit } from "@/lib/tabs/tabNavigationHistory";
import { supportsSqlFileExecution } from "@/lib/database/databaseCapabilities"; import { supportsSqlFileExecution } from "@/lib/database/databaseCapabilities";
import { classifyAiSqlExecution } from "@/lib/ai/aiSqlExecutionPolicy"; import { classifyAiSqlExecution } from "@/lib/ai/aiSqlExecutionPolicy";
import { buildAppendedEditorSql } from "@/lib/ai/aiSqlAppend"; import { buildAppendedEditorSql } from "@/lib/ai/aiSqlAppend";
@@ -287,6 +289,8 @@ const pendingAppCloseAction = ref<AppCloseAction | null>(null);
const pendingCloseActionChoice = ref(false); const pendingCloseActionChoice = ref(false);
const activeTab = computed(() => queryStore.tabs.find((t) => t.id === queryStore.activeTabId)); const activeTab = computed(() => queryStore.tabs.find((t) => t.id === queryStore.activeTabId));
const tabNavigationHistory = ref(createTabNavigationHistory());
let pendingTabHistoryNavigationId: string | null = null;
const externalSqlFileChanges = useExternalSqlFileChanges({ const externalSqlFileChanges = useExternalSqlFileChanges({
activeTab, activeTab,
@@ -725,6 +729,15 @@ watch(
}), }),
); );
} }
if (id) {
if (pendingTabHistoryNavigationId === id) pendingTabHistoryNavigationId = null;
else {
pendingTabHistoryNavigationId = null;
tabNavigationHistory.value = recordTabVisit(tabNavigationHistory.value, id);
}
} else {
pendingTabHistoryNavigationId = null;
}
if (id) newQueryContextSource.value = "tab"; if (id) newQueryContextSource.value = "tab";
if (id && driverStoreActive.value) driverStoreActive.value = false; if (id && driverStoreActive.value) driverStoreActive.value = false;
if (id && settingsStore.settingsPageActive) settingsStore.settingsPageActive = false; if (id && settingsStore.settingsPageActive) settingsStore.settingsPageActive = false;
@@ -2236,6 +2249,20 @@ function activateAdjacentTab(direction: -1 | 1): boolean {
return activateTabByIndex(nextIndex); return activateTabByIndex(nextIndex);
} }
function activateTabFromHistory(direction: -1 | 1): boolean {
const move = moveInTabNavigationHistory(tabNavigationHistory.value, direction, new Set(queryStore.tabs.map((tab) => tab.id)), queryStore.activeTabId);
if (!move) return false;
const previousHistory = tabNavigationHistory.value;
tabNavigationHistory.value = move.history;
pendingTabHistoryNavigationId = move.tabId;
if (activateQueryTab(move.tabId)) return true;
tabNavigationHistory.value = previousHistory;
pendingTabHistoryNavigationId = null;
return false;
}
function handleNativeSelectAll(e: KeyboardEvent) { function handleNativeSelectAll(e: KeyboardEvent) {
if (shouldBlockAppNativeSelectAll(e)) e.preventDefault(); if (shouldBlockAppNativeSelectAll(e)) e.preventDefault();
} }
@@ -2291,6 +2318,11 @@ async function handleKeydown(e: KeyboardEvent) {
} }
return; return;
} }
if (handleTabHistoryNavigationShortcut(e, shortcuts, activateTabFromHistory)) {
e.preventDefault();
e.stopPropagation();
return;
}
if (isSwitchToPreviousTabShortcut(e, shortcuts)) { if (isSwitchToPreviousTabShortcut(e, shortcuts)) {
if (activateAdjacentTab(-1)) { if (activateAdjacentTab(-1)) {
e.preventDefault(); e.preventDefault();
+2
View File
@@ -6298,6 +6298,8 @@ export default {
shortcutCloseTab: "Close tab", shortcutCloseTab: "Close tab",
shortcutFocusSearch: "Focus current view search", shortcutFocusSearch: "Focus current view search",
shortcutQuickOpen: "Quick open (search all database objects)", shortcutQuickOpen: "Quick open (search all database objects)",
shortcutNavigateTabHistoryBack: "Go back to the previously viewed tab",
shortcutNavigateTabHistoryForward: "Go forward to the next viewed tab",
shortcutSwitchToPreviousTab: "Switch to previous tab", shortcutSwitchToPreviousTab: "Switch to previous tab",
shortcutSwitchToNextTab: "Switch to next tab", shortcutSwitchToNextTab: "Switch to next tab",
shortcutSwitchToTab1: "Switch to tab 1", shortcutSwitchToTab1: "Switch to tab 1",
+2
View File
@@ -5991,6 +5991,8 @@ export default withEnglishFallback({
shortcutToggleSidebar: "Alternar barra lateral", shortcutToggleSidebar: "Alternar barra lateral",
shortcutFocusSearch: "Enfocar búsqueda de la vista actual", shortcutFocusSearch: "Enfocar búsqueda de la vista actual",
shortcutQuickOpen: "Abrir rápido (buscar todos los objetos de base de datos)", shortcutQuickOpen: "Abrir rápido (buscar todos los objetos de base de datos)",
shortcutNavigateTabHistoryBack: "Volver a la pestaña vista anteriormente",
shortcutNavigateTabHistoryForward: "Avanzar a la siguiente pestaña vista",
shortcutSwitchToPreviousTab: "Cambiar a la pestaña anterior", shortcutSwitchToPreviousTab: "Cambiar a la pestaña anterior",
shortcutSwitchToNextTab: "Cambiar a la pestaña siguiente", shortcutSwitchToNextTab: "Cambiar a la pestaña siguiente",
shortcutSwitchToTab1: "Cambiar a la pestaña 1", shortcutSwitchToTab1: "Cambiar a la pestaña 1",
+2
View File
@@ -5990,6 +5990,8 @@ export default withEnglishFallback({
shortcutCloseTab: "Chiudi scheda", shortcutCloseTab: "Chiudi scheda",
shortcutFocusSearch: "Focalizza la ricerca nella vista corrente", shortcutFocusSearch: "Focalizza la ricerca nella vista corrente",
shortcutQuickOpen: "Apertura rapida (ricerca tutti gli oggetti del database)", shortcutQuickOpen: "Apertura rapida (ricerca tutti gli oggetti del database)",
shortcutNavigateTabHistoryBack: "Torna alla scheda visualizzata in precedenza",
shortcutNavigateTabHistoryForward: "Vai avanti alla scheda visualizzata successiva",
shortcutSwitchToPreviousTab: "Passa alla scheda precedente", shortcutSwitchToPreviousTab: "Passa alla scheda precedente",
shortcutSwitchToNextTab: "Passa alla scheda successiva", shortcutSwitchToNextTab: "Passa alla scheda successiva",
shortcutSwitchToTab1: "Passa alla scheda 1", shortcutSwitchToTab1: "Passa alla scheda 1",
+2
View File
@@ -6005,6 +6005,8 @@ export default withEnglishFallback({
shortcutCloseTab: "タブを閉じる", shortcutCloseTab: "タブを閉じる",
shortcutFocusSearch: "現在のビューの検索にフォーカス", shortcutFocusSearch: "現在のビューの検索にフォーカス",
shortcutQuickOpen: "クイックオープン (すべてのデータベースオブジェクトを検索)", shortcutQuickOpen: "クイックオープン (すべてのデータベースオブジェクトを検索)",
shortcutNavigateTabHistoryBack: "前に表示したタブに戻る",
shortcutNavigateTabHistoryForward: "次に表示したタブに進む",
shortcutSwitchToPreviousTab: "前のタブに切り替え", shortcutSwitchToPreviousTab: "前のタブに切り替え",
shortcutSwitchToNextTab: "次のタブに切り替え", shortcutSwitchToNextTab: "次のタブに切り替え",
shortcutSwitchToTab1: "タブ 1 に切り替え", shortcutSwitchToTab1: "タブ 1 に切り替え",
+2
View File
@@ -5728,6 +5728,8 @@ export default withEnglishFallback({
shortcutCloseTab: "탭 닫기", shortcutCloseTab: "탭 닫기",
shortcutFocusSearch: "현재 화면 검색에 포커스", shortcutFocusSearch: "현재 화면 검색에 포커스",
shortcutQuickOpen: "빠른 열기 (모든 데이터베이스 객체 검색)", shortcutQuickOpen: "빠른 열기 (모든 데이터베이스 객체 검색)",
shortcutNavigateTabHistoryBack: "이전에 본 탭으로 돌아가기",
shortcutNavigateTabHistoryForward: "다음에 본 탭으로 이동",
shortcutSwitchToPreviousTab: "이전 탭으로 전환", shortcutSwitchToPreviousTab: "이전 탭으로 전환",
shortcutSwitchToNextTab: "다음 탭으로 전환", shortcutSwitchToNextTab: "다음 탭으로 전환",
shortcutSwitchToTab1: "탭 1로 전환", shortcutSwitchToTab1: "탭 1로 전환",
+2
View File
@@ -5993,6 +5993,8 @@ export default withEnglishFallback({
shortcutToggleSidebar: "Alternar barra lateral", shortcutToggleSidebar: "Alternar barra lateral",
shortcutFocusSearch: "Focar na pesquisa da tela atual", shortcutFocusSearch: "Focar na pesquisa da tela atual",
shortcutQuickOpen: "Abrir rapidamente (pesquisar todos os objetos do banco de dados)", shortcutQuickOpen: "Abrir rapidamente (pesquisar todos os objetos do banco de dados)",
shortcutNavigateTabHistoryBack: "Voltar para a aba visualizada anteriormente",
shortcutNavigateTabHistoryForward: "Avançar para a próxima aba visualizada",
shortcutSwitchToPreviousTab: "Alternar para a aba anterior", shortcutSwitchToPreviousTab: "Alternar para a aba anterior",
shortcutSwitchToNextTab: "Alternar para a próxima aba", shortcutSwitchToNextTab: "Alternar para a próxima aba",
shortcutSwitchToTab1: "Alternar para a aba 1", shortcutSwitchToTab1: "Alternar para a aba 1",
+2
View File
@@ -6287,6 +6287,8 @@ export default withEnglishFallback({
shortcutToggleSidebar: "切换侧边栏", shortcutToggleSidebar: "切换侧边栏",
shortcutFocusSearch: "聚焦当前页面搜索", shortcutFocusSearch: "聚焦当前页面搜索",
shortcutQuickOpen: "快速打开 (搜索所有数据库对象)", shortcutQuickOpen: "快速打开 (搜索所有数据库对象)",
shortcutNavigateTabHistoryBack: "返回到上一个查看的标签页",
shortcutNavigateTabHistoryForward: "前进到下一个查看的标签页",
shortcutSwitchToPreviousTab: "切换到左侧标签页", shortcutSwitchToPreviousTab: "切换到左侧标签页",
shortcutSwitchToNextTab: "切换到右侧标签页", shortcutSwitchToNextTab: "切换到右侧标签页",
shortcutSwitchToTab1: "切换到第 1 个标签页", shortcutSwitchToTab1: "切换到第 1 个标签页",
+2
View File
@@ -5301,6 +5301,8 @@ export default withEnglishFallback({
shortcutToggleSidebar: "切換側邊欄", shortcutToggleSidebar: "切換側邊欄",
shortcutFocusSearch: "聚焦目前頁面搜尋", shortcutFocusSearch: "聚焦目前頁面搜尋",
shortcutQuickOpen: "快速開啟 (搜尋所有資料庫物件)", shortcutQuickOpen: "快速開啟 (搜尋所有資料庫物件)",
shortcutNavigateTabHistoryBack: "返回到上一個檢視的分頁",
shortcutNavigateTabHistoryForward: "前進到下一個檢視的分頁",
shortcutSwitchToPreviousTab: "切換到左側分頁", shortcutSwitchToPreviousTab: "切換到左側分頁",
shortcutSwitchToNextTab: "切換到右側分頁", shortcutSwitchToNextTab: "切換到右側分頁",
shortcutSwitchToTab1: "切換到第 1 個分頁", shortcutSwitchToTab1: "切換到第 1 個分頁",
@@ -112,8 +112,8 @@ export function matchesShortcut(event: ShortcutLikeEvent, shortcut: string, plat
return matchesShortcutKey(event, key, platform); return matchesShortcutKey(event, key, platform);
} }
function actionShortcut(actionId: ShortcutActionId, shortcuts?: Partial<ShortcutSettings>): string { function actionShortcut(actionId: ShortcutActionId, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): string {
return normalizeShortcutSettings(shortcuts)[actionId]; return normalizeShortcutSettings(shortcuts, platform)[actionId];
} }
const SWITCH_TO_TAB_ACTIONS: ShortcutActionId[] = ["switchToTab1", "switchToTab2", "switchToTab3", "switchToTab4", "switchToTab5", "switchToTab6", "switchToTab7", "switchToTab8", "switchToTab9"]; 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)); 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 { export function isSwitchToPreviousTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("switchToPreviousTab", shortcuts)); return matchesShortcut(event, actionShortcut("switchToPreviousTab", shortcuts));
} }
+67 -11
View File
@@ -36,6 +36,8 @@ export type ShortcutActionId =
| "closeOtherTabs" | "closeOtherTabs"
| "focusSearch" | "focusSearch"
| "quickOpen" | "quickOpen"
| "navigateTabHistoryBack"
| "navigateTabHistoryForward"
| "switchToPreviousTab" | "switchToPreviousTab"
| "switchToNextTab" | "switchToNextTab"
| "switchToTab1" | "switchToTab1"
@@ -87,8 +89,19 @@ export function closeOtherTabsDefaultShortcut(platform = globalThis.navigator?.p
return isMacShortcutPlatform(platform) ? "Alt+Mod+W" : "Shift+Alt+W"; 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 LEGACY_CLOSE_TAB_DEFAULT = "Meta+W";
const TAB_NAVIGATION_HISTORY_ACTIONS: ShortcutActionId[] = ["navigateTabHistoryBack", "navigateTabHistoryForward"];
export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
{ {
@@ -301,6 +314,18 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
scope: "global", scope: "global",
defaultShortcut: "Mod+P", 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", id: "switchToPreviousTab",
labelKey: "settings.shortcutSwitchToPreviousTab", labelKey: "settings.shortcutSwitchToPreviousTab",
@@ -476,16 +501,36 @@ export function normalizeModifierOnlyShortcut(shortcut: string, fallback = ""):
return modifierOnlyShortcuts.has(normalized) ? normalized : fallback; return modifierOnlyShortcuts.has(normalized) ? normalized : fallback;
} }
export function normalizeShortcutSettings(settings?: Partial<ShortcutSettings>): ShortcutSettings { function hasExplicitShortcut(settings: Partial<ShortcutSettings> | undefined, actionId: ShortcutActionId): boolean {
return Object.fromEntries( 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) => { SHORTCUT_DEFINITIONS.map((definition) => {
const configuredValue = settings?.[definition.id]; const configuredValue = settings?.[definition.id];
let configured = typeof configuredValue === "string" ? configuredValue : definition.defaultShortcut; const platformDefault = shortcutDefaultForPlatform(definition, platform);
// 云同步会把另一平台的默认值当作显式配置带过来(macOS 的 Alt+Mod+W 到 let configured = typeof configuredValue === "string" ? configuredValue : platformDefault;
// Windows 上会还原成 Ctrl+Alt+W)。凡是平台默认集合内的值都视为"未 // 云同步会把另一平台的默认值当作显式配置带过来。平台默认集合内的值视为
// 自定义",按本机平台重新解析;用户真正自定义的其他组合原样保留 // 未自定义,按本机平台重新解析;用户真正自定义的其他组合原样保留
if (definition.id === "closeOtherTabs" && CLOSE_OTHER_TABS_PLATFORM_DEFAULTS.has(configured)) { const platformDefaults = PLATFORM_DEFAULT_SHORTCUTS[definition.id];
configured = definition.defaultShortcut; if (platformDefaults?.has(configured)) {
configured = platformDefault;
} }
// Meta+W was the old macOS-only default. Treat that exact value as a // Meta+W was the old macOS-only default. Treat that exact value as a
// legacy default so existing Windows/Linux settings adopt Ctrl+W. // legacy default so existing Windows/Linux settings adopt Ctrl+W.
@@ -496,6 +541,17 @@ export function normalizeShortcutSettings(settings?: Partial<ShortcutSettings>):
return [definition.id, normalized]; return [definition.id, normalized];
}), }),
) as ShortcutSettings; ) 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 { export function shortcutToCodeMirrorKey(shortcut: string): string {
@@ -521,11 +577,11 @@ export function formatShortcut(shortcut: string, platform = globalThis.navigator
.join("+"); .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; if (!shortcut) return null;
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId); const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
if (!definition) return null; 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; 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;
}
+3 -2
View File
@@ -12,7 +12,7 @@ import { DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID, type DataGridTypeColorScheme, norm
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { DEFAULT_QUERY_RESULT_MAX_ROWS, normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; import { DEFAULT_QUERY_RESULT_MAX_ROWS, normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
import { normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/connection/timeoutLimits"; import { normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/connection/timeoutLimits";
import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry"; import { needsTabNavigationHistoryShortcutMigration, normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget"; import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort"; import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort";
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay"; import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
@@ -1360,8 +1360,9 @@ export const useSettingsStore = defineStore("settings", () => {
const normalized = normalizeEditorSettings(savedSettings); const normalized = normalizeEditorSettings(savedSettings);
editorSettings.value = normalized; editorSettings.value = normalized;
const needsExecuteModeDefaultMigration = typeof savedSettings.executeModeDefaultVersion !== "number" || savedSettings.executeModeDefaultVersion < EXECUTE_MODE_CURRENT_DEFAULT_VERSION; const needsExecuteModeDefaultMigration = typeof savedSettings.executeModeDefaultVersion !== "number" || savedSettings.executeModeDefaultVersion < EXECUTE_MODE_CURRENT_DEFAULT_VERSION;
const needsTabNavigationShortcutMigration = needsTabNavigationHistoryShortcutMigration(savedSettings.shortcuts);
const savedUpdateDownloadSource = (saved as { updateDownloadSource?: unknown }).updateDownloadSource; const savedUpdateDownloadSource = (saved as { updateDownloadSource?: unknown }).updateDownloadSource;
if (savedUpdateDownloadSource === "atomgit" || needsExecuteModeDefaultMigration) { if (savedUpdateDownloadSource === "atomgit" || needsExecuteModeDefaultMigration || needsTabNavigationShortcutMigration) {
// Persist one-time migrations so removed or unsafe defaults cannot reappear. // Persist one-time migrations so removed or unsafe defaults cannot reappear.
await enqueueEditorSettingsSave().catch(() => {}); await enqueueEditorSettingsSave().catch(() => {});
} }
+2 -1
View File
@@ -48,7 +48,8 @@ DBX 的快捷键来自同一份可配置 action registry。**设置 → 快捷
| `Mod+T` | 新建查询 | | `Mod+T` | 新建查询 |
| `Mod+P` | 快速打开 | | `Mod+P` | 快速打开 |
| `Mod+W` | 关闭当前标签页 | | `Mod+W` | 关闭当前标签页 |
| `Shift+Mod+[` / `Shift+Mod+]` | 上一个/下一个标签页 | | `Ctrl+Alt+←` / `Ctrl+Alt+→` | 后退/前进标签页访问历史 |
| `Shift+Mod+[` / `Shift+Mod+]` | 左侧/右侧标签页 |
| `Mod+1` … `Mod+9` | 切换到指定标签页 | | `Mod+1` … `Mod+9` | 切换到指定标签页 |
| `Mod+B` | 显示或隐藏侧边栏 | | `Mod+B` | 显示或隐藏侧边栏 |
| `Mod+=` / `Mod+-` / `Mod+0` | 放大、缩小、重置 UI 缩放 | | `Mod+=` / `Mod+-` / `Mod+0` | 放大、缩小、重置 UI 缩放 |
+1
View File
@@ -48,6 +48,7 @@ Standard copy and paste still follow platform conventions. Copy Current Row, Del
| `Mod+T` | New query | | `Mod+T` | New query |
| `Mod+P` | Quick open | | `Mod+P` | Quick open |
| `Mod+W` | Close current tab | | `Mod+W` | Close current tab |
| `Ctrl+Alt+←` / `Ctrl+Alt+→` | Back/forward through tab visit history |
| `Shift+Mod+[` / `Shift+Mod+]` | Previous/next tab | | `Shift+Mod+[` / `Shift+Mod+]` | Previous/next tab |
| `Mod+1` … `Mod+9` | Switch to a numbered tab | | `Mod+1` … `Mod+9` | Switch to a numbered tab |
| `Mod+B` | Toggle the sidebar | | `Mod+B` | Toggle the sidebar |
+135 -1
View File
@@ -2,6 +2,7 @@ import { strict as assert } from "node:assert";
import { afterAll, beforeAll, test, vi } from "vitest"; import { afterAll, beforeAll, test, vi } from "vitest";
import { import {
eventToShortcut, eventToShortcut,
handleTabHistoryNavigationShortcut,
isBrowserReloadShortcut, isBrowserReloadShortcut,
isCancelSearchShortcut, isCancelSearchShortcut,
isCloseOtherTabsShortcut, isCloseOtherTabsShortcut,
@@ -11,6 +12,8 @@ import {
isEditSidebarConnectionShortcut, isEditSidebarConnectionShortcut,
isFocusSearchShortcut, isFocusSearchShortcut,
isModRShortcut, isModRShortcut,
isNavigateTabHistoryBackShortcut,
isNavigateTabHistoryForwardShortcut,
isNewQueryShortcut, isNewQueryShortcut,
isObjectSourceSaveShortcutTarget, isObjectSourceSaveShortcutTarget,
isOpenSettingsShortcut, isOpenSettingsShortcut,
@@ -28,7 +31,7 @@ import {
matchesShortcut, matchesShortcut,
switchToTabIndexFromShortcut, switchToTabIndexFromShortcut,
} from "../../apps/desktop/src/lib/editor/keyboardShortcuts.ts"; } from "../../apps/desktop/src/lib/editor/keyboardShortcuts.ts";
import { shortcutToCodeMirrorKey } from "../../apps/desktop/src/lib/editor/shortcutRegistry.ts"; import { DEFAULT_SHORTCUT_SETTINGS, findShortcutConflict, needsTabNavigationHistoryShortcutMigration, normalizeShortcutSettings, shortcutToCodeMirrorKey, tabNavigationHistoryDefaultShortcut } from "../../apps/desktop/src/lib/editor/shortcutRegistry.ts";
beforeAll(() => vi.stubGlobal("navigator", { platform: "Linux x86_64" })); beforeAll(() => vi.stubGlobal("navigator", { platform: "Linux x86_64" }));
afterAll(() => vi.unstubAllGlobals()); afterAll(() => vi.unstubAllGlobals());
@@ -82,6 +85,137 @@ test("matches Shift+Mod+brackets for switching adjacent tabs", () => {
assert.equal(isSwitchToNextTabShortcut({ key: "]", metaKey: true }), false); assert.equal(isSwitchToNextTabShortcut({ key: "]", metaKey: true }), false);
}); });
test("navigates backward and forward through tab visit history with Ctrl+Alt+Arrow keys", () => {
assert.equal(isNavigateTabHistoryBackShortcut({ key: "ArrowLeft", ctrlKey: true, altKey: true }), true);
assert.equal(isNavigateTabHistoryForwardShortcut({ key: "ArrowRight", ctrlKey: true, altKey: true }), true);
assert.equal(isNavigateTabHistoryBackShortcut({ key: "ArrowLeft", ctrlKey: true, altKey: true }, undefined, "Win32"), true);
assert.equal(isNavigateTabHistoryBackShortcut({ key: "ArrowLeft", metaKey: true, altKey: true }, undefined, "Win32"), true);
assert.equal(isNavigateTabHistoryBackShortcut({ key: "ArrowLeft", ctrlKey: true }), false);
assert.equal(isNavigateTabHistoryForwardShortcut({ key: "ArrowRight", altKey: true }), false);
});
test("keeps Ctrl and Mod distinct for tab history shortcuts on macOS", () => {
const event = { key: "ArrowLeft", ctrlKey: true, altKey: true };
assert.equal(isNavigateTabHistoryBackShortcut(event, undefined, "MacIntel"), true);
assert.equal(isNavigateTabHistoryBackShortcut({ ...event, metaKey: true }, undefined, "MacIntel"), false);
});
test("matches tab history defaults to the recorded shortcut format on each platform", () => {
const backEvent = { key: "ArrowLeft", ctrlKey: true, altKey: true };
const forwardEvent = { key: "ArrowRight", ctrlKey: true, altKey: true };
assert.equal(tabNavigationHistoryDefaultShortcut("back", "Win32"), "Mod+Alt+ArrowLeft");
assert.equal(tabNavigationHistoryDefaultShortcut("forward", "Linux x86_64"), "Mod+Alt+ArrowRight");
assert.equal(tabNavigationHistoryDefaultShortcut("back", "MacIntel"), "Ctrl+Alt+ArrowLeft");
assert.equal(tabNavigationHistoryDefaultShortcut("forward", "MacIntel"), "Ctrl+Alt+ArrowRight");
assert.equal(eventToShortcut(backEvent, "Win32"), tabNavigationHistoryDefaultShortcut("back", "Win32"));
assert.equal(eventToShortcut(forwardEvent, "Linux x86_64"), tabNavigationHistoryDefaultShortcut("forward", "Linux x86_64"));
assert.equal(eventToShortcut(backEvent, "MacIntel"), tabNavigationHistoryDefaultShortcut("back", "MacIntel"));
assert.equal(eventToShortcut(forwardEvent, "MacIntel"), tabNavigationHistoryDefaultShortcut("forward", "MacIntel"));
});
test("reports Ctrl and Mod as equivalent conflicts on Windows/Linux but not macOS", () => {
const backShortcut = eventToShortcut({ key: "ArrowLeft", ctrlKey: true, altKey: true }, "Win32")!;
const forwardShortcut = eventToShortcut({ key: "ArrowRight", ctrlKey: true, altKey: true }, "Win32")!;
const backSettings = { ...DEFAULT_SHORTCUT_SETTINGS, navigateTabHistoryBack: backShortcut, quickOpen: "Ctrl+Alt+ArrowLeft" };
const forwardSettings = { ...DEFAULT_SHORTCUT_SETTINGS, navigateTabHistoryForward: forwardShortcut, quickOpen: "Ctrl+Alt+ArrowRight" };
assert.equal(findShortcutConflict("quickOpen", backShortcut, backSettings, "Win32"), "navigateTabHistoryBack");
assert.equal(findShortcutConflict("quickOpen", forwardShortcut, forwardSettings, "Linux x86_64"), "navigateTabHistoryForward");
assert.equal(findShortcutConflict("quickOpen", "Mod+Alt+ArrowLeft", { ...DEFAULT_SHORTCUT_SETTINGS, navigateTabHistoryBack: "Ctrl+Alt+ArrowLeft", quickOpen: "Mod+Alt+ArrowLeft" }, "MacIntel"), null);
});
test("restores the local tab history default format after cross-platform sync", () => {
const currentBackDefault = tabNavigationHistoryDefaultShortcut("back");
const currentForwardDefault = tabNavigationHistoryDefaultShortcut("forward");
const otherBackDefault = currentBackDefault.startsWith("Mod+") ? "Ctrl+Alt+ArrowLeft" : "Mod+Alt+ArrowLeft";
const otherForwardDefault = currentForwardDefault.startsWith("Mod+") ? "Ctrl+Alt+ArrowRight" : "Mod+Alt+ArrowRight";
const normalized = normalizeShortcutSettings({
navigateTabHistoryBack: otherBackDefault,
navigateTabHistoryForward: otherForwardDefault,
});
assert.equal(normalized.navigateTabHistoryBack, currentBackDefault);
assert.equal(normalized.navigateTabHistoryForward, currentForwardDefault);
});
test("preserves existing actions and unbinds new history actions when defaults conflict", () => {
const normalized = normalizeShortcutSettings({
switchToPreviousTab: "Ctrl+Alt+ArrowLeft",
switchToNextTab: "Mod+Alt+ArrowRight",
});
assert.equal(normalized.switchToPreviousTab, "Ctrl+Alt+ArrowLeft");
assert.equal(normalized.switchToNextTab, "Mod+Alt+ArrowRight");
assert.equal(normalized.navigateTabHistoryBack, "");
assert.equal(normalized.navigateTabHistoryForward, "");
});
test("uses platform-aware defaults when migrating legacy shortcut settings", () => {
const windows = normalizeShortcutSettings({ switchToPreviousTab: "Ctrl+Alt+ArrowLeft" }, "Win32");
const mac = normalizeShortcutSettings({ switchToPreviousTab: "Mod+Alt+ArrowLeft" }, "MacIntel");
assert.equal(windows.navigateTabHistoryBack, "");
assert.equal(mac.navigateTabHistoryBack, "Ctrl+Alt+ArrowLeft");
});
test("only handles tab history shortcuts when navigation succeeds", () => {
const event = { key: "ArrowLeft", ctrlKey: true, altKey: true };
const directions: number[] = [];
let prevented = false;
prevented = handleTabHistoryNavigationShortcut(
event,
undefined,
(direction) => {
directions.push(direction);
return false;
},
"Win32",
);
assert.equal(prevented, false);
assert.deepEqual(directions, [-1]);
prevented = handleTabHistoryNavigationShortcut(
event,
undefined,
(direction) => {
directions.push(direction);
return true;
},
"Win32",
);
assert.equal(prevented, true);
assert.deepEqual(directions, [-1, -1]);
});
test("requests tab history migration only when legacy shortcut settings exist", () => {
assert.equal(needsTabNavigationHistoryShortcutMigration(), false);
assert.equal(needsTabNavigationHistoryShortcutMigration({}), true);
});
test("adds default bindings for new history actions when legacy shortcuts do not conflict", () => {
const normalized = normalizeShortcutSettings({
switchToPreviousTab: "Shift+Mod+[",
switchToNextTab: "Shift+Mod+]",
});
assert.equal(normalized.navigateTabHistoryBack, tabNavigationHistoryDefaultShortcut("back"));
assert.equal(normalized.navigateTabHistoryForward, tabNavigationHistoryDefaultShortcut("forward"));
});
test("preserves explicitly configured tab history shortcuts during migration", () => {
const backShortcut = tabNavigationHistoryDefaultShortcut("back");
const normalized = normalizeShortcutSettings({
navigateTabHistoryBack: backShortcut,
switchToPreviousTab: backShortcut,
});
assert.equal(normalized.navigateTabHistoryBack, backShortcut);
assert.equal(normalized.switchToPreviousTab, backShortcut);
});
test("matches Mod+number for switching to numbered tabs", () => { test("matches Mod+number for switching to numbered tabs", () => {
assert.equal(switchToTabIndexFromShortcut({ key: "1", metaKey: true }), 0); assert.equal(switchToTabIndexFromShortcut({ key: "1", metaKey: true }), 0);
assert.equal(switchToTabIndexFromShortcut({ key: "5", ctrlKey: true }), 4); assert.equal(switchToTabIndexFromShortcut({ key: "5", ctrlKey: true }), 4);
+44
View File
@@ -7,6 +7,7 @@ import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS } from "../../apps/desktop/src/lib
import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, SYSTEM_UI_FONT_FAMILY } from "../../apps/desktop/src/lib/app/appFonts.ts"; import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, SYSTEM_UI_FONT_FAMILY } from "../../apps/desktop/src/lib/app/appFonts.ts";
import { tableOpenPageLimit } from "../../apps/desktop/src/lib/table/tableOpenPageLimit.ts"; import { tableOpenPageLimit } from "../../apps/desktop/src/lib/table/tableOpenPageLimit.ts";
import { AI_PROVIDER_PRESETS, DEFAULT_EDITOR_SETTINGS, EXECUTE_MODE_CURRENT_DEFAULT_VERSION, normalizeAiConfig, normalizeEditorSettings, useSettingsStore } from "../../apps/desktop/src/stores/settingsStore.ts"; import { AI_PROVIDER_PRESETS, DEFAULT_EDITOR_SETTINGS, EXECUTE_MODE_CURRENT_DEFAULT_VERSION, normalizeAiConfig, normalizeEditorSettings, useSettingsStore } from "../../apps/desktop/src/stores/settingsStore.ts";
import { DEFAULT_SHORTCUT_SETTINGS, tabNavigationHistoryDefaultShortcut, type ShortcutSettings } from "../../apps/desktop/src/lib/editor/shortcutRegistry.ts";
const saveEditorSettingsMock = vi.hoisted(() => vi.fn()); const saveEditorSettingsMock = vi.hoisted(() => vi.fn());
vi.mock("../../apps/desktop/src/lib/backend/api", async (importOriginal) => { vi.mock("../../apps/desktop/src/lib/backend/api", async (importOriginal) => {
@@ -384,6 +385,8 @@ test("defaults shortcut settings", () => {
assert.equal(settings.shortcuts.newQuery, "Mod+T"); assert.equal(settings.shortcuts.newQuery, "Mod+T");
assert.equal(settings.shortcuts.openSettings, "Mod+,"); assert.equal(settings.shortcuts.openSettings, "Mod+,");
assert.equal(settings.shortcuts.focusSearch, "Mod+F"); assert.equal(settings.shortcuts.focusSearch, "Mod+F");
assert.equal(settings.shortcuts.navigateTabHistoryBack, tabNavigationHistoryDefaultShortcut("back"));
assert.equal(settings.shortcuts.navigateTabHistoryForward, tabNavigationHistoryDefaultShortcut("forward"));
assert.equal(settings.shortcuts.zoomInUi, "Mod+="); assert.equal(settings.shortcuts.zoomInUi, "Mod+=");
assert.equal(settings.shortcuts.zoomOutUi, "Mod+-"); assert.equal(settings.shortcuts.zoomOutUi, "Mod+-");
assert.equal(settings.shortcuts.resetUiZoom, "Mod+0"); assert.equal(settings.shortcuts.resetUiZoom, "Mod+0");
@@ -417,6 +420,47 @@ test("keeps saved shortcut overrides", () => {
assert.equal(settings.shortcuts.saveSql, "Mod+S"); assert.equal(settings.shortcuts.saveSql, "Mod+S");
}); });
test("preserves adjacent tab shortcuts and persists unbound history actions when upgrading legacy settings", async () => {
const legacyShortcuts: Partial<ShortcutSettings> = { ...DEFAULT_SHORTCUT_SETTINGS };
delete legacyShortcuts.navigateTabHistoryBack;
delete legacyShortcuts.navigateTabHistoryForward;
legacyShortcuts.switchToPreviousTab = tabNavigationHistoryDefaultShortcut("back");
legacyShortcuts.switchToNextTab = tabNavigationHistoryDefaultShortcut("forward");
await withMockLocalStorage(
{
"dbx-app-state:editor_settings": JSON.stringify({
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
shortcuts: legacyShortcuts,
}),
},
async () => {
setActivePinia(createPinia());
const migratedStore = useSettingsStore();
await migratedStore.initEditorSettings();
assert.equal(migratedStore.editorSettings.shortcuts.switchToPreviousTab, tabNavigationHistoryDefaultShortcut("back"));
assert.equal(migratedStore.editorSettings.shortcuts.switchToNextTab, tabNavigationHistoryDefaultShortcut("forward"));
assert.equal(migratedStore.editorSettings.shortcuts.navigateTabHistoryBack, "");
assert.equal(migratedStore.editorSettings.shortcuts.navigateTabHistoryForward, "");
await vi.waitFor(() => {
const saved = saveEditorSettingsMock.mock.calls.at(-1)?.[0] as { shortcuts?: ShortcutSettings } | undefined;
assert.equal(saved?.shortcuts?.navigateTabHistoryBack, "");
assert.equal(saved?.shortcuts?.navigateTabHistoryForward, "");
});
setActivePinia(createPinia());
const reloadedStore = useSettingsStore();
await reloadedStore.initEditorSettings();
assert.equal(reloadedStore.editorSettings.shortcuts.switchToPreviousTab, tabNavigationHistoryDefaultShortcut("back"));
assert.equal(reloadedStore.editorSettings.shortcuts.switchToNextTab, tabNavigationHistoryDefaultShortcut("forward"));
assert.equal(reloadedStore.editorSettings.shortcuts.navigateTabHistoryBack, "");
assert.equal(reloadedStore.editorSettings.shortcuts.navigateTabHistoryForward, "");
},
);
});
test("defaults sidebar activation to single click", () => { test("defaults sidebar activation to single click", () => {
assert.equal(DEFAULT_EDITOR_SETTINGS.sidebarActivation, "single"); assert.equal(DEFAULT_EDITOR_SETTINGS.sidebarActivation, "single");
assert.equal(normalizeEditorSettings({}).sidebarActivation, "single"); assert.equal(normalizeEditorSettings({}).sidebarActivation, "single");
+60
View File
@@ -0,0 +1,60 @@
import { strict as assert } from "node:assert";
import { test } from "vitest";
import { createTabNavigationHistory, moveInTabNavigationHistory, recordTabVisit } from "../../apps/desktop/src/lib/tabs/tabNavigationHistory.ts";
test("navigates backward and forward in tab visit order", () => {
let history = createTabNavigationHistory();
history = recordTabVisit(history, "A");
history = recordTabVisit(history, "D");
history = recordTabVisit(history, "B");
const openTabs = new Set(["A", "B", "C", "D"]);
const backToD = moveInTabNavigationHistory(history, -1, openTabs);
assert.equal(backToD?.tabId, "D");
history = backToD!.history;
const backToA = moveInTabNavigationHistory(history, -1, openTabs);
assert.equal(backToA?.tabId, "A");
history = backToA!.history;
const forwardToD = moveInTabNavigationHistory(history, 1, openTabs);
assert.equal(forwardToD?.tabId, "D");
history = forwardToD!.history;
assert.equal(moveInTabNavigationHistory(history, 1, openTabs)?.tabId, "B");
});
test("clears the forward branch after visiting a tab manually", () => {
let history = createTabNavigationHistory();
history = recordTabVisit(history, "A");
history = recordTabVisit(history, "D");
history = recordTabVisit(history, "B");
history = moveInTabNavigationHistory(history, -1, new Set(["A", "B", "C", "D"]))!.history;
history = recordTabVisit(history, "C");
assert.deepEqual(history, { entries: ["A", "D", "C"], index: 2 });
assert.equal(moveInTabNavigationHistory(history, 1, new Set(["A", "B", "C", "D"])), null);
});
test("skips closed tabs while navigating", () => {
let history = createTabNavigationHistory();
history = recordTabVisit(history, "A");
history = recordTabVisit(history, "B");
history = recordTabVisit(history, "C");
const openTabs = new Set(["A", "C"]);
const backToA = moveInTabNavigationHistory(history, -1, openTabs);
assert.equal(backToA?.tabId, "A");
assert.equal(moveInTabNavigationHistory(backToA!.history, 1, openTabs)?.tabId, "C");
});
test("skips history entries matching the active tab", () => {
let history = createTabNavigationHistory();
history = recordTabVisit(history, "A");
history = recordTabVisit(history, "D");
history = recordTabVisit(history, "B");
history = recordTabVisit(history, "D");
assert.equal(moveInTabNavigationHistory(history, -1, new Set(["A", "D"]), "D")?.tabId, "A");
});