feat(tabs): add Ctrl+Tab MRU switcher

This commit is contained in:
Allen authored and GitHub committed 2026-08-20 22:12:52 +08:00
1 parent 81872165da
commit 7ce992b8f8
18 files changed
+615 -1

No files matched your search

+86
View File
@@ -87,9 +87,13 @@ import {
isZoomInShortcut,
isZoomOutShortcut,
switchToTabIndexFromShortcut,
tabSwitcherDirectionFromShortcut,
} from "@/lib/editor/keyboardShortcuts";
import { isPreviewTab } from "@/lib/tabs/tabPresentation";
import { createTabNavigationHistory, moveInTabNavigationHistory, recordTabVisit } from "@/lib/tabs/tabNavigationHistory";
import { initialTabSwitcherSelection, moveTabSwitcherSelection, tabSwitcherOrder } from "@/lib/tabs/tabSwitcher";
import { createTabSwitcherKeyboardController } from "@/lib/tabs/tabSwitcherKeyboard";
import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay";
import { supportsSqlFileExecution } from "@/lib/database/databaseCapabilities";
import { classifyAiSqlExecution } from "@/lib/ai/aiSqlExecutionPolicy";
import { buildAppendedEditorSql } from "@/lib/ai/aiSqlAppend";
@@ -134,6 +138,7 @@ const UpdateDialog = defineAsyncComponent(() => import("@/components/layout/Upda
const CloseActionPromptDialog = defineAsyncComponent(() => import("@/components/layout/CloseActionPromptDialog.vue"));
const LoginPage = defineAsyncComponent(() => import("@/components/auth/LoginPage.vue"));
const QuickOpenDialog = defineAsyncComponent(() => import("@/components/quick-open/QuickOpenDialog.vue"));
const TabSwitcherDialog = defineAsyncComponent(() => import("@/components/tabs/TabSwitcherDialog.vue"));
const QueryEditorDdlViewDialog = defineAsyncComponent(() => import("@/components/objects/DdlViewDialog.vue"));
const QueryEditorObjectSourceDialog = defineAsyncComponent(() => import("@/components/objects/ObjectSourceDialog.vue"));
@@ -226,6 +231,8 @@ const settingsReturnSurface = ref<"query" | "driverStore" | "welcome">("welcome"
const showDriverStore = computed(() => driverStoreTabOpen.value && driverStoreActive.value);
const showSettingsPage = computed(() => settingsPageTabOpen.value && settingsStore.settingsPageActive);
const showQuickOpen = ref(false);
const showTabSwitcher = ref(false);
const tabSwitcherIndex = ref(0);
const agentDriverUpdateCount = ref(0);
const showHistory = ref(false);
const showAiPanel = ref(safeLocalStorageGet("dbx-ai-panel-open") === "true");
@@ -2318,6 +2325,66 @@ function activateTabFromHistory(direction: -1 | 1): boolean {
return false;
}
const tabSwitcherTabs = computed(() => tabSwitcherOrder(queryStore.tabs, queryStore.recentTabIds));
const tabSwitcherShortcutHint = computed(() => formatShortcutDisplay(settingsStore.editorSettings.shortcuts.tabSwitcher));
function handleTabSwitcherShortcut(direction: -1 | 1, e: KeyboardEvent): boolean {
if (!queryStore.tabs.length) return false;
if (!showTabSwitcher.value) {
tabSwitcherKeyboard.rememberOpeningEvent(e);
tabSwitcherIndex.value = initialTabSwitcherSelection(tabSwitcherTabs.value.length);
showTabSwitcher.value = true;
} else {
tabSwitcherIndex.value = moveTabSwitcherSelection(tabSwitcherIndex.value, direction, tabSwitcherTabs.value.length);
}
return true;
}
function closeTabSwitcher(commit: boolean) {
tabSwitcherKeyboard.reset();
if (!showTabSwitcher.value) return;
showTabSwitcher.value = false;
if (!commit) return;
const tab = tabSwitcherTabs.value[tabSwitcherIndex.value];
if (tab) activateQueryTab(tab.id);
}
function handleKeyup(e: KeyboardEvent) {
tabSwitcherKeyboard.handleKeyup(e);
}
function handleTabSwitcherKeydownCapture(e: KeyboardEvent) {
tabSwitcherKeyboard.handleKeydownCapture(e);
}
function handleTabSwitcherWindowBlur() {
tabSwitcherKeyboard.handleWindowBlur();
}
function handleTabSwitcherVisibilityChange() {
tabSwitcherKeyboard.handleVisibilityChange(document.visibilityState);
}
function handleTabSwitcherSelect(tabId: string) {
tabSwitcherKeyboard.reset();
showTabSwitcher.value = false;
activateQueryTab(tabId);
}
function handleTabSwitcherOpenChange(open: boolean) {
if (!open) closeTabSwitcher(false);
}
const tabSwitcherKeyboard = createTabSwitcherKeyboardController({
isOpen: () => showTabSwitcher.value,
shortcut: () => settingsStore.editorSettings.shortcuts.tabSwitcher,
move: (direction) => {
tabSwitcherIndex.value = moveTabSwitcherSelection(tabSwitcherIndex.value, direction, tabSwitcherTabs.value.length);
},
commit: () => closeTabSwitcher(true),
cancel: () => closeTabSwitcher(false),
});
function handleNativeSelectAll(e: KeyboardEvent) {
if (shouldBlockAppNativeSelectAll(e)) e.preventDefault();
}
@@ -2326,6 +2393,15 @@ async function handleKeydown(e: KeyboardEvent) {
if (e.defaultPrevented) return;
const shortcuts = settingsStore.editorSettings.shortcuts;
const tabSwitcherDirection = tabSwitcherDirectionFromShortcut(e, shortcuts);
if (showTabSwitcher.value) return;
if (tabSwitcherDirection) {
e.preventDefault();
e.stopPropagation();
handleTabSwitcherShortcut(tabSwitcherDirection, e);
return;
}
const switchTabIndex = switchToTabIndexFromShortcut(e, shortcuts);
if (isOpenSettingsShortcut(e, shortcuts)) {
@@ -2599,7 +2675,11 @@ onMounted(async () => {
applyTheme();
void applyUiScale(settingsStore.editorSettings.uiScale);
window.addEventListener("keydown", handleNativeSelectAll, true);
window.addEventListener("keydown", handleTabSwitcherKeydownCapture, true);
window.addEventListener("keydown", handleKeydown);
window.addEventListener("keyup", handleKeyup, true);
window.addEventListener("blur", handleTabSwitcherWindowBlur);
document.addEventListener("visibilitychange", handleTabSwitcherVisibilityChange);
window.addEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
window.addEventListener("dbx-mcp-status-changed", handleMcpStatusChanged);
if (isDesktop) {
@@ -2667,7 +2747,12 @@ onUnmounted(() => {
clearInterval(updateCheckTimer);
}
window.removeEventListener("keydown", handleNativeSelectAll, true);
window.removeEventListener("keydown", handleTabSwitcherKeydownCapture, true);
window.removeEventListener("keydown", handleKeydown);
window.removeEventListener("keyup", handleKeyup, true);
window.removeEventListener("blur", handleTabSwitcherWindowBlur);
document.removeEventListener("visibilitychange", handleTabSwitcherVisibilityChange);
tabSwitcherKeyboard.reset();
window.removeEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
window.removeEventListener("dbx-mcp-status-changed", handleMcpStatusChanged);
document.removeEventListener("contextmenu", handleContextMenu);
@@ -3020,6 +3105,7 @@ onUnmounted(() => {
<ExternalSqlFileChangeDialog :prompt="externalSqlFilePrompt" @decide="externalSqlFileChanges.resolvePrompt" />
<CloseActionPromptDialog v-if="isDesktop && showCloseActionPrompt" :open="showCloseActionPrompt" @update:open="handleCloseActionPromptOpenChange" @quit="chooseQuit" @minimize="chooseMinimize" />
<QuickOpenDialog :open="showQuickOpen" @update:open="showQuickOpen = $event" @select="handleQuickOpenSelect" />
<TabSwitcherDialog :open="showTabSwitcher" :tabs="tabSwitcherTabs" :selected-index="tabSwitcherIndex" :shortcut-hint="tabSwitcherShortcutHint" @update:open="handleTabSwitcherOpenChange" @update:selected-index="tabSwitcherIndex = $event" @select="handleTabSwitcherSelect" />
</div>
<Teleport to="body">
<FileText