feat(shortcuts): add close other tabs shortcut

This commit is contained in:
vrustx 2026-07-20 23:31:26 +08:00 committed by GitHub
parent e143a76e1f
commit d2975fc015
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 127 additions and 5 deletions

View File

@ -53,6 +53,7 @@ import type { ConnectionConfig, ObjectSourceKind, QueryTab } from "@/types/datab
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
import {
isBrowserReloadShortcut,
isCloseOtherTabsShortcut,
isCloseTabShortcut,
isExecuteSqlShortcut,
isFocusSearchShortcut,
@ -183,6 +184,7 @@ const aiPanelReady = ref(false);
const { sidebarWidth, aiPanelWidth, historyWidth, sqlLibraryWidth, sqlFilePanelWidth, startSidebarResize, startAiPanelResize, startHistoryResize, startSqlLibraryResize, startSqlFilePanelResize } = usePanelResize();
const aiAssistantRef = ref<AiAssistantHandle | null>(null);
const appSidebarRef = ref<InstanceType<typeof AppSidebar> | null>(null);
const appTabBarRef = ref<InstanceType<typeof AppTabBar> | null>(null);
const contentAreaRef = ref<InstanceType<typeof ContentArea> | null>(null);
const selectedSql = ref("");
@ -1760,6 +1762,12 @@ function handleKeydown(e: KeyboardEvent) {
}
return;
}
if (isCloseOtherTabsShortcut(e, shortcuts)) {
e.preventDefault();
e.stopPropagation();
appTabBarRef.value?.closeOtherActiveTabs();
return;
}
if (isCloseTabShortcut(e, shortcuts)) {
e.preventDefault();
if (showSettingsPage.value) {
@ -2051,6 +2059,7 @@ onUnmounted(() => {
<div :class="isClassicLayout ? 'flex-1 min-w-0 overflow-hidden' : 'flex-1 min-w-0 overflow-hidden rounded-md border border-border/80 bg-background'">
<div class="h-full flex flex-col min-w-0">
<AppTabBar
ref="appTabBarRef"
:driver-store-open="driverStoreTabOpen"
:driver-store-active="driverStoreActive"
:settings-page-open="settingsPageTabOpen"

View File

@ -185,6 +185,26 @@ function closeAllRegularSurfaces() {
closeSpecialRegularSurfaces();
}
function closeOtherActiveTabs() {
if (props.settingsPageActive) {
queryStore.closeRegularTabs();
closeSpecialRegularSurfaces("settings");
return;
}
if (props.driverStoreActive) {
queryStore.closeRegularTabs();
closeSpecialRegularSurfaces("driverStore");
return;
}
const tab = queryStore.tabs.find((item) => item.id === queryStore.activeTabId);
if (!tab) return;
if (tab.pinned) queryStore.closeOtherFixedTabs(tab.id);
else closeOtherRegularTabsFromTab(tab);
}
defineExpose({ closeOtherActiveTabs });
function getSpecialRegularTabMenuItems(surface: SpecialRegularSurface): ContextMenuItem[] {
const keep = surface;
const closeCurrent = surface === "driverStore" ? () => emit("close-driver-store") : () => emit("close-settings-page");
@ -208,6 +228,7 @@ function getSpecialRegularTabMenuItems(surface: SpecialRegularSurface): ContextM
},
disabled: closeOtherDisabled,
icon: X,
shortcut: settingsStore.editorSettings.shortcuts.closeOtherTabs,
},
{
label: closeAllLabel,
@ -270,6 +291,7 @@ function getTabMenuItems(tab: QueryTab): ContextMenuItem[] {
action: closeOtherAction,
disabled: closeOtherDisabled,
icon: X,
shortcut: settingsStore.editorSettings.shortcuts.closeOtherTabs,
},
{
label: closeAllLabel,

View File

@ -19,6 +19,11 @@ describe("shortcut display", () => {
expect(shortcutDisplayKeys("Mod+Alt+Enter", "MacIntel")).toEqual(["⌘", "⌥", "↵"]);
});
it("formats the close other tabs shortcut by platform", () => {
expect(formatShortcutDisplay("Alt+Mod+W", "MacIntel")).toBe("⌥ ⌘ W");
expect(formatShortcutDisplay("Shift+Alt+W", "Win32")).toBe("Shift + Alt + W");
});
it("formats shortcut pills with platform separators", () => {
expect(formatShortcutDisplay("Shift+Alt+ArrowUp", "Win32")).toBe("Shift + Alt + ↑");
expect(formatShortcutDisplay("Shift+Alt+ArrowUp", "MacIntel")).toBe("⇧ ⌥ ↑");

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_SHORTCUT_SETTINGS, SHORTCUT_DEFINITIONS, findShortcutConflict, formatShortcut, normalizeModifierOnlyShortcut, normalizeShortcutSettings, shortcutToCodeMirrorKey, type ShortcutActionId } from "@/lib/editor/shortcutRegistry";
import { closeOtherTabsDefaultShortcut, DEFAULT_SHORTCUT_SETTINGS, SHORTCUT_DEFINITIONS, findShortcutConflict, formatShortcut, normalizeModifierOnlyShortcut, normalizeShortcutSettings, shortcutToCodeMirrorKey, type ShortcutActionId } from "@/lib/editor/shortcutRegistry";
describe("shortcutRegistry editor actions", () => {
const formatterEditorActionIds: ShortcutActionId[] = [
@ -30,6 +30,20 @@ describe("shortcutRegistry editor actions", () => {
expect(formatShortcut(DEFAULT_SHORTCUT_SETTINGS.openDataInNewTab, "MacIntel")).toBe("Alt");
});
it("resolves the close-other-tabs default per platform and heals cross-platform synced defaults", () => {
// 本测试环境darwin默认应为 macOS 组合
expect(DEFAULT_SHORTCUT_SETTINGS.closeOtherTabs).toBe(closeOtherTabsDefaultShortcut());
expect(closeOtherTabsDefaultShortcut("MacIntel")).toBe("Alt+Mod+W");
// Windows/Linux 不含 Ctrl+AltAltGr也不含 Ctrl+Shift+W浏览器关窗保留键
expect(closeOtherTabsDefaultShortcut("Win32")).toBe("Shift+Alt+W");
expect(closeOtherTabsDefaultShortcut("Linux x86_64")).toBe("Shift+Alt+W");
// 云同步把另一平台的默认值带过来:视为未自定义,按本机平台还原
expect(normalizeShortcutSettings({ closeOtherTabs: "Alt+Mod+W" }).closeOtherTabs).toBe(closeOtherTabsDefaultShortcut());
expect(normalizeShortcutSettings({ closeOtherTabs: "Shift+Alt+W" }).closeOtherTabs).toBe(closeOtherTabsDefaultShortcut());
// 用户真正自定义的组合原样保留
expect(normalizeShortcutSettings({ closeOtherTabs: "Shift+Mod+O" }).closeOtherTabs).toBe("Shift+Mod+O");
});
it("normalizes custom, cleared, and invalid modifier-only shortcuts", () => {
expect(normalizeShortcutSettings({ openDataInNewTab: "Shift" }).openDataInNewTab).toBe("Shift");
expect(normalizeShortcutSettings({ openDataInNewTab: "" }).openDataInNewTab).toBe("");

View File

@ -3,6 +3,7 @@ import { normalizeShortcutSettings, type ShortcutActionId, type ShortcutSettings
export interface ShortcutLikeEvent {
key: string;
code?: string;
metaKey?: boolean;
ctrlKey?: boolean;
altKey?: boolean;
@ -16,6 +17,18 @@ function normalizeKey(key: string): string {
return key.length === 1 ? key.toLowerCase() : key;
}
function matchesShortcutKey(event: ShortcutLikeEvent, key: string, platform = globalThis.navigator?.platform || ""): boolean {
if (normalizeKey(event.key) === normalizeKey(key)) return true;
// KeyboardEvent.code 回退event.key 随布局/修饰键变形时按物理键位匹配
// macOS Option+字母 → 变形字符如 ⌥W="∑";俄文等非拉丁布局 → "Ц")。
// 仅限 Alt 组合键场景
if (!event.altKey || !/^Key[A-Z]$/.test(event.code ?? "") || !/^[A-Z]$/i.test(key)) return false;
// 非 macOS 的 Ctrl+Alt 是 AltGr 特征:用户可能在输入字符(如波兰语
// AltGr+W → "ł"),按 code 强制匹配会让全局快捷键在打字时误触发
if (!isMacShortcutPlatform(platform) && event.ctrlKey) return false;
return event.code!.slice(3).toLowerCase() === key.toLowerCase();
}
function shortcutKeyName(key: string): string | null {
if (key === " ") return "Space";
if (key === "+") return "Plus";
@ -64,7 +77,7 @@ export function matchesModifierOnlyShortcut(event: Omit<ShortcutLikeEvent, "key"
return false;
}
export function matchesShortcut(event: ShortcutLikeEvent, shortcut: string): boolean {
export function matchesShortcut(event: ShortcutLikeEvent, shortcut: string, platform = globalThis.navigator?.platform || ""): boolean {
if (event.isComposing || !shortcut) return false;
const parts = parseShortcutParts(shortcut);
const key = parts[parts.length - 1] ?? "";
@ -82,7 +95,7 @@ export function matchesShortcut(event: ShortcutLikeEvent, shortcut: string): boo
if (!!event.altKey !== modifiers.has("Alt")) return false;
if (!!event.shiftKey !== modifiers.has("Shift")) return false;
return normalizeKey(event.key) === normalizeKey(key);
return matchesShortcutKey(event, key, platform);
}
function actionShortcut(actionId: ShortcutActionId, shortcuts?: Partial<ShortcutSettings>): string {
@ -99,6 +112,10 @@ export function isCloseTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial
return matchesShortcut(event, actionShortcut("closeTab", shortcuts));
}
export function isCloseOtherTabsShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>, platform = globalThis.navigator?.platform || ""): boolean {
return matchesShortcut(event, actionShortcut("closeOtherTabs", shortcuts), platform);
}
export function isSendSelectionToAiShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("sendSelectionToAi", shortcuts));
}

View File

@ -1,4 +1,4 @@
import { parseShortcutStrokes, shortcutDisplayParts } from "@/lib/editor/shortcutDisplay";
import { isMacShortcutPlatform, parseShortcutStrokes, shortcutDisplayParts } from "@/lib/editor/shortcutDisplay";
export type ShortcutActionId =
| "executeSql"
@ -25,6 +25,7 @@ export type ShortcutActionId =
| "newQuery"
| "openSettings"
| "closeTab"
| "closeOtherTabs"
| "focusSearch"
| "quickOpen"
| "switchToPreviousTab"
@ -65,6 +66,19 @@ export interface ShortcutDefinition {
export type ShortcutSettings = Record<ShortcutActionId, string>;
// closeOtherTabs 的平台相关默认键。Windows/Linux 不用 Alt+Mod= Ctrl+Alt
// 与国际键盘 AltGr 字符输入冲突),也不用 Ctrl+Shift+W浏览器保留的关窗键
// Web 形态不可拦截closeTab 默认 Meta+W 同理Shift+Alt+W 无浏览器保留
// 冲突Firefox accesskey 同为 Alt+Shift+字母,属正常应用快捷键区)。
// 已知取舍Windows 的 Alt+Shift 布局切换只在单独按下并释放时触发,
// Alt+Shift+字母会正常送达应用,多语言用户如遇干扰可自定义改键。
// macOS 的 ⌥⌘W 无上述问题
export function closeOtherTabsDefaultShortcut(platform = globalThis.navigator?.platform || ""): string {
return isMacShortcutPlatform(platform) ? "Alt+Mod+W" : "Shift+Alt+W";
}
const CLOSE_OTHER_TABS_PLATFORM_DEFAULTS = new Set(["Alt+Mod+W", "Shift+Alt+W"]);
export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
{
id: "executeSql",
@ -210,6 +224,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
scope: "global",
defaultShortcut: "Meta+W",
},
{
id: "closeOtherTabs",
labelKey: "contextMenu.closeOtherTabs",
scope: "global",
defaultShortcut: closeOtherTabsDefaultShortcut(),
},
{
id: "focusSearch",
labelKey: "settings.shortcutFocusSearch",
@ -389,7 +409,13 @@ export function normalizeShortcutSettings(settings?: Partial<ShortcutSettings>):
return Object.fromEntries(
SHORTCUT_DEFINITIONS.map((definition) => {
const configuredValue = settings?.[definition.id];
const configured = typeof configuredValue === "string" ? configuredValue : definition.defaultShortcut;
let configured = typeof configuredValue === "string" ? configuredValue : definition.defaultShortcut;
// 云同步会把另一平台的默认值当作显式配置带过来macOS 的 Alt+Mod+W 到
// Windows 上会还原成 Ctrl+Alt+W。凡是平台默认集合内的值都视为"未
// 自定义",按本机平台重新解析;用户真正自定义的其他组合原样保留
if (definition.id === "closeOtherTabs" && CLOSE_OTHER_TABS_PLATFORM_DEFAULTS.has(configured)) {
configured = definition.defaultShortcut;
}
const normalized = definition.inputKind === "modifier-only" ? normalizeModifierOnlyShortcut(configured, definition.defaultShortcut) : configured;
return [definition.id, normalized];
}),

View File

@ -4,6 +4,7 @@ import {
eventToShortcut,
isBrowserReloadShortcut,
isCancelSearchShortcut,
isCloseOtherTabsShortcut,
isCloseTabShortcut,
isCopySidebarSelectionShortcut,
isExecuteSqlShortcut,
@ -24,6 +25,7 @@ import {
isToggleTransposeShortcut,
isZoomInShortcut,
isZoomOutShortcut,
matchesShortcut,
switchToTabIndexFromShortcut,
} from "../../apps/desktop/src/lib/editor/keyboardShortcuts.ts";
import { shortcutToCodeMirrorKey } from "../../apps/desktop/src/lib/editor/shortcutRegistry.ts";
@ -135,6 +137,33 @@ test("ignores Ctrl+W for closing query tabs", () => {
assert.equal(isCloseTabShortcut({ key: "w", ctrlKey: true }), false);
});
test("matches platform shortcuts for closing other tabs", () => {
// macOS 默认 ⌥⌘WOption 会把 event.key 变形⌥W → "∑"),按 code 回退匹配。
// 平台默认集合内的值会被 normalize 按本机平台还原(云同步自愈),因此
// 默认组合的匹配行为用 matchesShortcut 直接断言,自定义组合走完整入口
assert.equal(matchesShortcut({ key: "∑", code: "KeyW", altKey: true, metaKey: true }, "Alt+Mod+W", "MacIntel"), true);
assert.equal(matchesShortcut({ key: "w", metaKey: true }, "Alt+Mod+W", "MacIntel"), false);
// Windows/Linux 默认 Shift+Alt+W不含 Ctrl+Alt 避开 AltGr不含 Ctrl+Shift+W 避开浏览器关窗保留键)
assert.equal(matchesShortcut({ key: "W", altKey: true, shiftKey: true }, "Shift+Alt+W", "Win32"), true);
assert.equal(matchesShortcut({ key: "w", altKey: true }, "Shift+Alt+W", "Win32"), false);
// 非拉丁布局(俄文 Ц 在物理 KeyW 上):无 Ctrl 的 Alt 组合按 code 回退匹配,默认键不失效
assert.equal(matchesShortcut({ key: "Ц", code: "KeyW", altKey: true, shiftKey: true }, "Shift+Alt+W", "Win32"), true);
// 用户自定义组合(非平台默认集合)经完整入口匹配
assert.equal(isCloseOtherTabsShortcut({ key: "o", ctrlKey: true, shiftKey: true }, { closeOtherTabs: "Shift+Mod+O" }, "Win32"), true);
});
test("altgr character input never triggers close other tabs on windows layouts", () => {
// 波兰语等布局AltGr+W= Ctrl+Alt+W产生字符 "ł"event.key 不是 "w"。
// 即使用户自定义了 Alt+Mod+Wcode 回退在非 macOS 平台禁用,不得按物理
// KeyW 强制匹配——否则用户输入文本会误触发关闭其他标签页
assert.equal(matchesShortcut({ key: "ł", code: "KeyW", altKey: true, ctrlKey: true }, "Alt+Mod+W", "Win32"), false);
assert.equal(matchesShortcut({ key: "ę", code: "KeyE", altKey: true, ctrlKey: true }, "Alt+Mod+E", "Linux x86_64"), false);
// 显式按下字母本身key 就是 "w")仍正常匹配自定义组合
assert.equal(matchesShortcut({ key: "w", altKey: true, ctrlKey: true }, "Alt+Mod+W", "Win32"), true);
// 经完整入口:非平台默认集合的自定义组合在 AltGr 布局下同样不误触发
assert.equal(isCloseOtherTabsShortcut({ key: "ę", code: "KeyE", altKey: true, ctrlKey: true }, { closeOtherTabs: "Alt+Mod+E" }, "Win32"), false);
});
test("matches Ctrl+F for focusing search", () => {
assert.equal(isFocusSearchShortcut({ key: "f", ctrlKey: true }), true);
});