🐛 fix(shortcuts): 同步侧边栏搜索快捷键提示

- 侧边栏 v2 搜索入口改为读取用户快捷键配置
- 修复搜索入口固定显示默认 ⌘K 的问题
- 按 macOS 语义使用 Cmd+F 作为查找类快捷键
- 移除快捷键描述中的硬编码默认组合
- 补充快捷键展示与平台冲突判断测试
This commit is contained in:
Syngnat
2026-06-01 11:03:22 +08:00
parent 35b7fdf96b
commit 999efa5947
5 changed files with 85 additions and 35 deletions

View File

@@ -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) {

View File

@@ -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<typeof import('../utils/appearance')>('../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('<kbd>Ctrl</kbd>');
expect(markup).toContain('<kbd></kbd>');
expect(markup).toContain('<kbd>K</kbd>');
expect(source).toContain("const focusSidebarSearchShortcut = resolveShortcutDisplay(shortcutOptions, 'focusSidebarSearch', activeShortcutPlatform);");
expect(source).not.toContain('<kbd>⌘</kbd>');
expect(source).not.toContain('<kbd>K</kbd>');
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(<Sidebar uiVersion="v2" />);
expect(markup).toContain('gn-v2-search-shortcut');
expect(markup).toContain('<kbd>⌘</kbd>');
expect(markup).toContain('<kbd>F</kbd>');
expect(markup).not.toContain('<kbd>K</kbd>');
});
it('keeps the v2 command search footer hints tied to real prefix actions', () => {
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');

View File

@@ -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<TreeNode[]>([]);
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<{
>
<SearchOutlined />
<span>... AI</span>
<span className="gn-v2-search-shortcut" aria-hidden="true">
<kbd>{primaryShortcutModifierLabel}</kbd>
<kbd>K</kbd>
</span>
{focusSidebarSearchShortcutTokens.length > 0 ? (
<span className="gn-v2-search-shortcut" aria-hidden="true">
{focusSidebarSearchShortcutTokens.map((token, index) => (
<kbd key={`${token}-${index}`}>{token}</kbd>
))}
</span>
) : null}
</button>
) : (
<Input

View File

@@ -82,6 +82,18 @@ describe('findReservedConflicts', () => {
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 },

View File

@@ -159,7 +159,7 @@ export const SHORTCUT_ACTION_META: Record<ShortcutAction, ShortcutActionMeta> =
},
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<ConflictContext, string> = {
@@ -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 }));
};