diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8e275ae..21ef1ba 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2129,7 +2129,7 @@ function App() { for (const action of SHORTCUT_ACTION_ORDER) { const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); if (!binding?.enabled || !binding.combo) continue; - const conflicts = findReservedConflicts(normalizeShortcutCombo(binding.combo)); + const conflicts = findReservedConflicts(normalizeShortcutCombo(binding.combo), activeShortcutPlatform); if (conflicts.length > 0) { map[action] = conflicts; } @@ -3001,7 +3001,7 @@ function App() { return; } - const reservedConflicts = findReservedConflicts(normalizedCombo); + const reservedConflicts = findReservedConflicts(normalizedCombo, activeShortcutPlatform); if (reservedConflicts.length > 0) { const { hasMonaco, hasOther, monacoLabels, otherLabels, otherContexts } = splitConflictsByContext(reservedConflicts); if (hasMonaco) { diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 950dc0c..f3a6795 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -66,6 +66,7 @@ const mocks = vi.hoisted(() => ({ blur: 0, uiVersion: 'legacy', } as any, + shortcutOptions: null as any, }, })); @@ -148,7 +149,7 @@ vi.mock('../store', () => ({ setSidebarTablePinned: mocks.noop, addSqlLog: mocks.noop, sqlLogs: [], - shortcutOptions: cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS), + shortcutOptions: mocks.state.shortcutOptions ?? cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS), setAIPanelVisible: mocks.noop, addAIContext: mocks.noop, }), @@ -183,6 +184,14 @@ vi.mock('../../wailsjs/runtime/runtime', () => ({ EventsOn: mocks.noop, })); +vi.mock('../utils/appearance', async () => { + const actual = await vi.importActual('../utils/appearance'); + return { + ...actual, + isMacLikePlatform: () => true, + }; +}); + describe('Sidebar locate toolbar', () => { beforeEach(() => { mocks.state.connections = []; @@ -203,6 +212,7 @@ describe('Sidebar locate toolbar', () => { blur: 0, uiVersion: 'legacy', }; + mocks.state.shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS); }); it('resolves the table name used by the sidebar copy action', () => { @@ -388,8 +398,11 @@ describe('Sidebar locate toolbar', () => { expect(markup).toContain('gn-v2-explorer-command-trigger'); expect(markup).toContain('搜索表、连接、动作... 或问 AI'); expect(markup).toContain('gn-v2-search-shortcut'); - expect(markup).toContain('Ctrl'); + expect(markup).toContain(''); expect(markup).toContain('K'); + expect(source).toContain("const focusSidebarSearchShortcut = resolveShortcutDisplay(shortcutOptions, 'focusSidebarSearch', activeShortcutPlatform);"); + expect(source).not.toContain(''); + expect(source).not.toContain('K'); expect(markup).toContain('gn-v2-explorer-filter-tabs'); expect(markup).toContain('全部'); expect(markup).toContain('视图'); @@ -439,6 +452,18 @@ describe('Sidebar locate toolbar', () => { expect(contextMenuFunction).not.toContain('setActiveContext'); }); + it('renders the v2 search shortcut from the user shortcut settings', () => { + mocks.state.shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS); + mocks.state.shortcutOptions.focusSidebarSearch.mac = { combo: 'Meta+F', enabled: true }; + + const markup = renderToStaticMarkup(); + + expect(markup).toContain('gn-v2-search-shortcut'); + expect(markup).toContain(''); + expect(markup).toContain('F'); + expect(markup).not.toContain('K'); + }); + it('keeps the v2 command search footer hints tied to real prefix actions', () => { const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 651f2b2..0c29332 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -80,7 +80,7 @@ import { resolveConnectionAccentColor, resolveConnectionIconType } from '../util import { buildJVMTabTitle } from '../utils/jvmRuntimePresentation'; import { buildJVMDiagnosticActionDescriptor, buildJVMMonitoringActionDescriptors } from '../utils/jvmSidebarActions'; import { buildTableSelectQuery } from '../utils/objectQueryTemplates'; -import { getShortcutPlatform, getShortcutPrimaryModifierDisplayLabel, resolveShortcutDisplay } from '../utils/shortcuts'; +import { getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts'; import { buildExternalSQLDirectoryId, buildExternalSQLRootNode, buildExternalSQLTabId, type ExternalSQLTreeNode } from '../utils/externalSqlTree'; import JVMModeBadge from './jvm/JVMModeBadge'; import { @@ -914,7 +914,10 @@ const Sidebar: React.FC<{ const disableLocalBackdropFilter = isMacLikePlatform(); const autoFetchVisible = useAutoFetchVisibility(); const activeShortcutPlatform = getShortcutPlatform(isMacLikePlatform()); - const primaryShortcutModifierLabel = getShortcutPrimaryModifierDisplayLabel(activeShortcutPlatform); + const focusSidebarSearchShortcut = resolveShortcutDisplay(shortcutOptions, 'focusSidebarSearch', activeShortcutPlatform); + const focusSidebarSearchShortcutTokens = focusSidebarSearchShortcut === '-' + ? [] + : focusSidebarSearchShortcut.match(/Ctrl|Alt|Shift|Esc|Space|[⌘⌃⌥⇧↵↑↓←→]|[^+]/g) ?? []; const [treeData, setTreeData] = useState([]); const activeTab = useMemo(() => tabs.find(tab => tab.id === activeTabId) || null, [tabs, activeTabId]); const activeTabLocateRequest = useMemo(() => normalizeSidebarLocateObjectRequestFromTab(activeTab), [activeTab]); @@ -8003,10 +8006,13 @@ const Sidebar: React.FC<{ > 搜索表、连接、动作... 或问 AI - + {focusSidebarSearchShortcutTokens.length > 0 ? ( + + ) : null} ) : ( { const results = findReservedConflicts('Ctrl+F'); expect(results[0].monacoCommandId).toBe('actions.find'); }); + + it('uses Command instead of Control for macOS find shortcut conflicts', () => { + expect(findReservedConflicts('Ctrl+F', 'mac')).toEqual([]); + expect(findReservedConflicts('Meta+F', 'mac')[0]).toMatchObject({ + label: '编辑器查找', + monacoCommandId: 'actions.find', + }); + expect(findReservedConflicts('Ctrl+F', 'windows')[0]).toMatchObject({ + label: '编辑器查找', + monacoCommandId: 'actions.find', + }); + }); }); // ─── describeConflictContext ───────────────────────────────────────── @@ -160,6 +172,12 @@ describe('shortcut defaults', () => { }); }); + it('keeps configurable shortcut descriptions free of hardcoded shortcut labels', () => { + Object.values(SHORTCUT_ACTION_META).forEach((meta) => { + expect(meta.description).not.toMatch(/⌘|⌃|Ctrl|Meta|Cmd|Command|Alt\+/); + }); + }); + it('uses Navicat-inspired defaults separately for macOS and Windows/Linux', () => { expect(DEFAULT_SHORTCUT_OPTIONS.runQuery).toEqual({ mac: { combo: 'Meta+R', enabled: true }, diff --git a/frontend/src/utils/shortcuts.ts b/frontend/src/utils/shortcuts.ts index cddd846..68ffb11 100644 --- a/frontend/src/utils/shortcuts.ts +++ b/frontend/src/utils/shortcuts.ts @@ -159,7 +159,7 @@ export const SHORTCUT_ACTION_META: Record = }, toggleMacFullscreen: { label: '切换原生全屏', - description: 'macOS 原生窗口控制模式下的全屏切换(⌃⌘F)', + description: 'macOS 原生窗口控制模式下的全屏切换', platformOnly: 'mac', }, resetWindowZoom: { @@ -504,6 +504,7 @@ export interface ReservedShortcut { label: string; context: ConflictContext; monacoCommandId?: string; + platforms?: ShortcutPlatform[]; } export interface ConflictInfo { @@ -522,29 +523,29 @@ export const RESERVED_SHORTCUTS: ReservedShortcut[] = [ { combo: 'Ctrl+Shift+N', label: '浏览器新建隐身窗口', context: 'global' }, // Monaco editor built-in shortcuts - { combo: 'Ctrl+F', label: '编辑器查找', context: 'monaco', monacoCommandId: 'actions.find' }, - { combo: 'Meta+F', label: '编辑器查找', context: 'monaco', monacoCommandId: 'actions.find' }, - { combo: 'Ctrl+H', label: '编辑器替换', context: 'monaco', monacoCommandId: 'editor.action.startFindReplaceAction' }, - { combo: 'Meta+H', label: '编辑器替换', context: 'monaco', monacoCommandId: 'editor.action.startFindReplaceAction' }, - { combo: 'Ctrl+G', label: '编辑器跳转行', context: 'monaco', monacoCommandId: 'editor.action.gotoLine' }, - { combo: 'Meta+G', label: '编辑器跳转行', context: 'monaco', monacoCommandId: 'editor.action.gotoLine' }, - { combo: 'Ctrl+P', label: '编辑器快速打开', context: 'monaco', monacoCommandId: 'actions.quickOpen' }, - { combo: 'Meta+P', label: '编辑器快速打开', context: 'monaco', monacoCommandId: 'actions.quickOpen' }, - { combo: 'Ctrl+Shift+F', label: '编辑器全局查找', context: 'monaco', monacoCommandId: 'actions.quickOpenNavigate' }, - { combo: 'Meta+Shift+F', label: '编辑器全局查找', context: 'monaco', monacoCommandId: 'actions.quickOpenNavigate' }, - { combo: 'Ctrl+D', label: '编辑器添加选区', context: 'monaco', monacoCommandId: 'editor.action.addSelectionToNextFindMatch' }, - { combo: 'Meta+D', label: '编辑器添加选区', context: 'monaco', monacoCommandId: 'editor.action.addSelectionToNextFindMatch' }, - { combo: 'Ctrl+Shift+K', label: '编辑器删除行', context: 'monaco', monacoCommandId: 'editor.action.deleteLines' }, - { combo: 'Meta+Shift+K', label: '编辑器删除行', context: 'monaco', monacoCommandId: 'editor.action.deleteLines' }, - { combo: 'Ctrl+Enter', label: '编辑器在下方插入行', context: 'monaco', monacoCommandId: 'editor.action.insertLineAfter' }, - { combo: 'Meta+Enter', label: '编辑器在下方插入行', context: 'monaco', monacoCommandId: 'editor.action.insertLineAfter' }, - { combo: 'Ctrl+Shift+Enter', label: '编辑器在上方插入行', context: 'monaco', monacoCommandId: 'editor.action.insertLineBefore' }, - { combo: 'Meta+Shift+Enter', label: '编辑器在上方插入行', context: 'monaco', monacoCommandId: 'editor.action.insertLineBefore' }, + { combo: 'Ctrl+F', label: '编辑器查找', context: 'monaco', monacoCommandId: 'actions.find', platforms: ['windows'] }, + { combo: 'Meta+F', label: '编辑器查找', context: 'monaco', monacoCommandId: 'actions.find', platforms: ['mac'] }, + { combo: 'Ctrl+H', label: '编辑器替换', context: 'monaco', monacoCommandId: 'editor.action.startFindReplaceAction', platforms: ['windows'] }, + { combo: 'Meta+H', label: '编辑器替换', context: 'monaco', monacoCommandId: 'editor.action.startFindReplaceAction', platforms: ['mac'] }, + { combo: 'Ctrl+G', label: '编辑器跳转行', context: 'monaco', monacoCommandId: 'editor.action.gotoLine', platforms: ['windows'] }, + { combo: 'Meta+G', label: '编辑器跳转行', context: 'monaco', monacoCommandId: 'editor.action.gotoLine', platforms: ['mac'] }, + { combo: 'Ctrl+P', label: '编辑器快速打开', context: 'monaco', monacoCommandId: 'actions.quickOpen', platforms: ['windows'] }, + { combo: 'Meta+P', label: '编辑器快速打开', context: 'monaco', monacoCommandId: 'actions.quickOpen', platforms: ['mac'] }, + { combo: 'Ctrl+Shift+F', label: '编辑器全局查找', context: 'monaco', monacoCommandId: 'actions.quickOpenNavigate', platforms: ['windows'] }, + { combo: 'Meta+Shift+F', label: '编辑器全局查找', context: 'monaco', monacoCommandId: 'actions.quickOpenNavigate', platforms: ['mac'] }, + { combo: 'Ctrl+D', label: '编辑器添加选区', context: 'monaco', monacoCommandId: 'editor.action.addSelectionToNextFindMatch', platforms: ['windows'] }, + { combo: 'Meta+D', label: '编辑器添加选区', context: 'monaco', monacoCommandId: 'editor.action.addSelectionToNextFindMatch', platforms: ['mac'] }, + { combo: 'Ctrl+Shift+K', label: '编辑器删除行', context: 'monaco', monacoCommandId: 'editor.action.deleteLines', platforms: ['windows'] }, + { combo: 'Meta+Shift+K', label: '编辑器删除行', context: 'monaco', monacoCommandId: 'editor.action.deleteLines', platforms: ['mac'] }, + { combo: 'Ctrl+Enter', label: '编辑器在下方插入行', context: 'monaco', monacoCommandId: 'editor.action.insertLineAfter', platforms: ['windows'] }, + { combo: 'Meta+Enter', label: '编辑器在下方插入行', context: 'monaco', monacoCommandId: 'editor.action.insertLineAfter', platforms: ['mac'] }, + { combo: 'Ctrl+Shift+Enter', label: '编辑器在上方插入行', context: 'monaco', monacoCommandId: 'editor.action.insertLineBefore', platforms: ['windows'] }, + { combo: 'Meta+Shift+Enter', label: '编辑器在上方插入行', context: 'monaco', monacoCommandId: 'editor.action.insertLineBefore', platforms: ['mac'] }, { combo: 'F2', label: '编辑器重命名符号', context: 'monaco', monacoCommandId: 'editor.action.rename' }, // DataGrid shortcuts - { combo: 'Ctrl+C', label: '数据表格复制', context: 'datagrid' }, - { combo: 'Meta+C', label: '数据表格复制', context: 'datagrid' }, + { combo: 'Ctrl+C', label: '数据表格复制', context: 'datagrid', platforms: ['windows'] }, + { combo: 'Meta+C', label: '数据表格复制', context: 'datagrid', platforms: ['mac'] }, ]; const CONTEXT_DESCRIPTION: Record = { @@ -572,14 +573,14 @@ export const splitConflictsByContext = (conflicts: ConflictInfo[]) => { }; export const findReservedConflict = (normalizedCombo: string): ConflictInfo | null => { - const conflict = RESERVED_SHORTCUTS.find((r) => r.combo === normalizedCombo); + const conflict = findReservedConflicts(normalizedCombo)[0]; if (!conflict) return null; - return { label: conflict.label, context: conflict.context, monacoCommandId: conflict.monacoCommandId }; + return conflict; }; -export const findReservedConflicts = (normalizedCombo: string): ConflictInfo[] => { +export const findReservedConflicts = (normalizedCombo: string, platform?: ShortcutPlatform): ConflictInfo[] => { return RESERVED_SHORTCUTS - .filter((r) => r.combo === normalizedCombo) + .filter((r) => r.combo === normalizedCombo && (!platform || !r.platforms || r.platforms.includes(platform))) .map((r) => ({ label: r.label, context: r.context, monacoCommandId: r.monacoCommandId })); };