From f74ad02b9df7c6b2d365360b5dccb0a9d40b3d8f Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 8 Jul 2026 20:30:31 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E9=99=90?= =?UTF-8?q?=E5=88=B6=20macOS=20=E6=90=9C=E7=B4=A2=E4=BB=85=E5=93=8D?= =?UTF-8?q?=E5=BA=94=20Cmd+F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 拦截 Monaco 内置 Cmd+E find-with-selection 行为 - 保留默认 Cmd+E 当前语句选择逻辑,禁用或改键时不触发搜索 - 增加 SQL 编辑器快捷键回归测试并加固 Monaco 测试包装 --- frontend/src/components/MonacoEditor.tsx | 34 +++++++- .../QueryEditor.external-sql-save.test.tsx | 33 ++++++++ frontend/src/components/QueryEditor.tsx | 81 ++++++++++++++++--- 3 files changed, 136 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/MonacoEditor.tsx b/frontend/src/components/MonacoEditor.tsx index 64596346..bad1b679 100644 --- a/frontend/src/components/MonacoEditor.tsx +++ b/frontend/src/components/MonacoEditor.tsx @@ -168,7 +168,8 @@ const installOceanBaseOracleNavigationFallback = (editor: any) => { }; const patchQueryEditorAiInlineRightArrowFallback = (editor: any, monaco: any) => { - const originalAddCommand = editor?.addCommand?.bind?.(editor); + const rawAddCommand = editor?.addCommand; + const originalAddCommand = rawAddCommand?.bind?.(editor); if (!originalAddCommand || !monaco?.KeyCode?.RightArrow) { return; } @@ -180,7 +181,7 @@ const patchQueryEditorAiInlineRightArrowFallback = (editor: any, monaco: any) => configurable: true, }); - editor.addCommand = (keybinding: any, handler: any, context: any) => { + const patchedAddCommand = (keybinding: any, handler: any, context: any) => { if ( keybinding === monaco.KeyCode.RightArrow && context === QUERY_EDITOR_AI_INLINE_CONTEXT_KEY @@ -200,6 +201,32 @@ const patchQueryEditorAiInlineRightArrowFallback = (editor: any, monaco: any) => } return originalAddCommand(keybinding, handler, context); }; + + if (rawAddCommand?.mock) { + for (const propertyName of [ + 'mock', + 'mockClear', + 'mockReset', + 'mockRestore', + 'mockImplementation', + 'mockImplementationOnce', + 'mockName', + 'getMockName', + ]) { + if (!(propertyName in rawAddCommand)) { + continue; + } + Object.defineProperty(patchedAddCommand, propertyName, { + configurable: true, + get: () => { + const value = rawAddCommand[propertyName]; + return typeof value === 'function' ? value.bind(rawAddCommand) : value; + }, + }); + } + } + + editor.addCommand = patchedAddCommand; }; const installPrintableInputFallback = (editor: any, monaco: any) => { @@ -207,8 +234,9 @@ const installPrintableInputFallback = (editor: any, monaco: any) => { if (!editorDomNode || editor.__gonaviPrintableInputFallbackInstalled) { return; } + const TextAreaElement = typeof HTMLTextAreaElement === 'undefined' ? null : HTMLTextAreaElement; const input = editorDomNode.querySelector?.('textarea.inputarea, .inputarea textarea, textarea') as HTMLTextAreaElement | null; - if (!(input instanceof HTMLTextAreaElement)) { + if (!TextAreaElement || !(input instanceof TextAreaElement)) { return; } Object.defineProperty(editor, '__gonaviPrintableInputFallbackInstalled', { diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 04215b42..d140c5aa 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -4481,6 +4481,39 @@ describe('QueryEditor external SQL save', () => { ).not.toContain('gonavi:find-active-query'); }); + it('keeps SQL editor search on Cmd+F only and suppresses Monaco Cmd+E find-with-selection', async () => { + storeState.shortcutOptions.selectCurrentStatement.mac = { enabled: false, combo: '' }; + storeState.shortcutOptions.selectCurrentStatement.windows = { enabled: false, combo: '' }; + + await act(async () => { + create(); + }); + (window.dispatchEvent as any).mockClear(); + (document.execCommand as any).mockClear(); + + expect(findEditorAction('gonavi.findInEditor')).toMatchObject({ + keybindings: [2048 | 70], + }); + + const suppressMacFindAction = findEditorAction('gonavi.suppressMacFindWithSelection'); + expect(suppressMacFindAction).toMatchObject({ + keybindings: [2048 | 69], + }); + + await act(async () => { + suppressMacFindAction.run(); + await Promise.resolve(); + }); + + expect( + (window.dispatchEvent as any).mock.calls.map((call: any[]) => call[0]?.type), + ).not.toContain('gonavi:find-active-query'); + expect(document.execCommand).not.toHaveBeenCalled(); + }); + it('intercepts Ctrl/Cmd+D at window level and duplicates the current line below', async () => { storeState.shortcutOptions.duplicateCurrentLine.mac = { enabled: true, combo: 'Meta+D' }; storeState.shortcutOptions.duplicateCurrentLine.windows = { enabled: true, combo: 'Ctrl+D' }; diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 46331efd..be416242 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -170,6 +170,8 @@ const QUERY_EDITOR_MONACO_FIND_OPTIONS = { addExtraSpaceOnTop: true, } as const; const QUERY_EDITOR_NATIVE_SELECT_CURRENT_LINE_EVENT = 'gonavi:native-select-current-line'; +const QUERY_EDITOR_MAC_FIND_WITH_SELECTION_COMBO = 'Meta+E'; +const QUERY_EDITOR_MAC_FIND_WITH_SELECTION_GUARD_ACTION_ID = 'gonavi.suppressMacFindWithSelection'; const QUERY_EDITOR_AI_INLINE_DEBOUNCE_MS = 90; const QUERY_EDITOR_AI_INLINE_CONTEXT_KEY = 'gonaviAiInlineSuggestionVisible'; @@ -1131,6 +1133,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const monacoRef = useRef(null); const runQueryActionRef = useRef(null); const selectCurrentStatementActionRef = useRef(null); + const macFindWithSelectionGuardActionRef = useRef(null); const duplicateCurrentLineActionRef = useRef(null); const saveQueryActionRef = useRef(null); const findInEditorActionRef = useRef(null); @@ -4308,6 +4311,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc objectHoverActionRef.current = null; triggerSqlAiCompletionActionRef.current?.dispose?.(); triggerSqlAiCompletionActionRef.current = null; + macFindWithSelectionGuardActionRef.current?.dispose?.(); + macFindWithSelectionGuardActionRef.current = null; triggerSqlAiCompletionKeydownDisposableRef.current?.dispose?.(); triggerSqlAiCompletionKeydownDisposableRef.current = null; triggerAiInlineCompletionRef.current = null; @@ -4369,6 +4374,30 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc } } + const macFindWithSelectionGuardKeyBinding = activeShortcutPlatform === 'mac' + ? comboToMonacoKeyBinding( + QUERY_EDITOR_MAC_FIND_WITH_SELECTION_COMBO, monaco.KeyMod, monaco.KeyCode, + ) + : null; + if (macFindWithSelectionGuardKeyBinding) { + macFindWithSelectionGuardActionRef.current = editor.addAction({ + id: QUERY_EDITOR_MAC_FIND_WITH_SELECTION_GUARD_ACTION_ID, + label: 'GoNavi: Suppress macOS Cmd+E Find with Selection', + keybindings: [ + macFindWithSelectionGuardKeyBinding.keyMod + | macFindWithSelectionGuardKeyBinding.keyCode, + ], + run: () => { + if ( + selectStatementBinding?.enabled + && normalizeShortcutCombo(selectStatementBinding.combo) === QUERY_EDITOR_MAC_FIND_WITH_SELECTION_COMBO + ) { + void handleSelectCurrentStatement(); + } + }, + }); + } + const duplicateLineBinding = duplicateCurrentLineShortcutBinding; if (duplicateLineBinding?.enabled && duplicateLineBinding.combo) { const keyBinding = comboToMonacoKeyBinding( @@ -6734,21 +6763,51 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc selectCurrentStatementActionRef.current.dispose(); selectCurrentStatementActionRef.current = null; } + if (macFindWithSelectionGuardActionRef.current) { + macFindWithSelectionGuardActionRef.current.dispose(); + macFindWithSelectionGuardActionRef.current = null; + } const editor = editorRef.current; const monaco = monacoRef.current; if (!editor || !monaco) return; const binding = selectCurrentStatementShortcutBinding; - if (!binding?.enabled || !binding.combo) return; + if (binding?.enabled && binding.combo) { + const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode); + if (keyBinding) { + selectCurrentStatementActionRef.current = editor.addAction({ + id: 'gonavi.selectCurrentStatement', + label: buildQueryEditorMonacoActionLabel('app.shortcuts.action.selectCurrentStatement.label'), + keybindings: [keyBinding.keyMod | keyBinding.keyCode], + run: handleSelectCurrentStatement, + }); + } + } - const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode); - if (keyBinding) { - selectCurrentStatementActionRef.current = editor.addAction({ - id: 'gonavi.selectCurrentStatement', - label: buildQueryEditorMonacoActionLabel('app.shortcuts.action.selectCurrentStatement.label'), - keybindings: [keyBinding.keyMod | keyBinding.keyCode], - run: handleSelectCurrentStatement, + const macFindWithSelectionGuardKeyBinding = activeShortcutPlatform === 'mac' + ? comboToMonacoKeyBinding( + QUERY_EDITOR_MAC_FIND_WITH_SELECTION_COMBO, + monaco.KeyMod, + monaco.KeyCode, + ) + : null; + if (macFindWithSelectionGuardKeyBinding) { + macFindWithSelectionGuardActionRef.current = editor.addAction({ + id: QUERY_EDITOR_MAC_FIND_WITH_SELECTION_GUARD_ACTION_ID, + label: 'GoNavi: Suppress macOS Cmd+E Find with Selection', + keybindings: [ + macFindWithSelectionGuardKeyBinding.keyMod + | macFindWithSelectionGuardKeyBinding.keyCode, + ], + run: () => { + if ( + binding?.enabled + && normalizeShortcutCombo(binding.combo) === QUERY_EDITOR_MAC_FIND_WITH_SELECTION_COMBO + ) { + void handleSelectCurrentStatement(); + } + }, }); } @@ -6757,8 +6816,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc selectCurrentStatementActionRef.current.dispose(); selectCurrentStatementActionRef.current = null; } + if (macFindWithSelectionGuardActionRef.current) { + macFindWithSelectionGuardActionRef.current.dispose(); + macFindWithSelectionGuardActionRef.current = null; + } }; - }, [languagePreference, selectCurrentStatementShortcutBinding, handleSelectCurrentStatement]); + }, [activeShortcutPlatform, languagePreference, selectCurrentStatementShortcutBinding, handleSelectCurrentStatement]); useEffect(() => { if (duplicateCurrentLineActionRef.current) {