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
@@ -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();
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -6005,6 +6005,8 @@ export default withEnglishFallback({
|
|||||||
shortcutCloseTab: "タブを閉じる",
|
shortcutCloseTab: "タブを閉じる",
|
||||||
shortcutFocusSearch: "現在のビューの検索にフォーカス",
|
shortcutFocusSearch: "現在のビューの検索にフォーカス",
|
||||||
shortcutQuickOpen: "クイックオープン (すべてのデータベースオブジェクトを検索)",
|
shortcutQuickOpen: "クイックオープン (すべてのデータベースオブジェクトを検索)",
|
||||||
|
shortcutNavigateTabHistoryBack: "前に表示したタブに戻る",
|
||||||
|
shortcutNavigateTabHistoryForward: "次に表示したタブに進む",
|
||||||
shortcutSwitchToPreviousTab: "前のタブに切り替え",
|
shortcutSwitchToPreviousTab: "前のタブに切り替え",
|
||||||
shortcutSwitchToNextTab: "次のタブに切り替え",
|
shortcutSwitchToNextTab: "次のタブに切り替え",
|
||||||
shortcutSwitchToTab1: "タブ 1 に切り替え",
|
shortcutSwitchToTab1: "タブ 1 に切り替え",
|
||||||
|
|||||||
@@ -5728,6 +5728,8 @@ export default withEnglishFallback({
|
|||||||
shortcutCloseTab: "탭 닫기",
|
shortcutCloseTab: "탭 닫기",
|
||||||
shortcutFocusSearch: "현재 화면 검색에 포커스",
|
shortcutFocusSearch: "현재 화면 검색에 포커스",
|
||||||
shortcutQuickOpen: "빠른 열기 (모든 데이터베이스 객체 검색)",
|
shortcutQuickOpen: "빠른 열기 (모든 데이터베이스 객체 검색)",
|
||||||
|
shortcutNavigateTabHistoryBack: "이전에 본 탭으로 돌아가기",
|
||||||
|
shortcutNavigateTabHistoryForward: "다음에 본 탭으로 이동",
|
||||||
shortcutSwitchToPreviousTab: "이전 탭으로 전환",
|
shortcutSwitchToPreviousTab: "이전 탭으로 전환",
|
||||||
shortcutSwitchToNextTab: "다음 탭으로 전환",
|
shortcutSwitchToNextTab: "다음 탭으로 전환",
|
||||||
shortcutSwitchToTab1: "탭 1로 전환",
|
shortcutSwitchToTab1: "탭 1로 전환",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -6287,6 +6287,8 @@ export default withEnglishFallback({
|
|||||||
shortcutToggleSidebar: "切换侧边栏",
|
shortcutToggleSidebar: "切换侧边栏",
|
||||||
shortcutFocusSearch: "聚焦当前页面搜索",
|
shortcutFocusSearch: "聚焦当前页面搜索",
|
||||||
shortcutQuickOpen: "快速打开 (搜索所有数据库对象)",
|
shortcutQuickOpen: "快速打开 (搜索所有数据库对象)",
|
||||||
|
shortcutNavigateTabHistoryBack: "返回到上一个查看的标签页",
|
||||||
|
shortcutNavigateTabHistoryForward: "前进到下一个查看的标签页",
|
||||||
shortcutSwitchToPreviousTab: "切换到左侧标签页",
|
shortcutSwitchToPreviousTab: "切换到左侧标签页",
|
||||||
shortcutSwitchToNextTab: "切换到右侧标签页",
|
shortcutSwitchToNextTab: "切换到右侧标签页",
|
||||||
shortcutSwitchToTab1: "切换到第 1 个标签页",
|
shortcutSwitchToTab1: "切换到第 1 个标签页",
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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(() => {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 缩放 |
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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");
|
||||||
|
});
|
||||||
Reference in new issue
Block a user