feat(query-editor): 新增编辑器内搜索入口

- 在查询编辑器工具栏新增搜索入口并直连 Monaco 查找\n- 接管 Ctrl/Cmd+F,优先打开编辑器内搜索而不是浏览器默认查找\n- 补充多语言文案与静态测试覆盖\n\nFixes #539
This commit is contained in:
Syngnat
2026-06-29 20:00:27 +08:00
parent a7fdab5bb2
commit 93006d8423
11 changed files with 171 additions and 0 deletions

View File

@@ -731,6 +731,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const runQueryActionRef = useRef<any>(null);
const selectCurrentStatementActionRef = useRef<any>(null);
const saveQueryActionRef = useRef<any>(null);
const findInEditorActionRef = useRef<any>(null);
const formatSqlActionRef = useRef<any>(null);
const aiContextMenuActionDisposablesRef = useRef<any[]>([]);
const toggleQueryResultsPanelActionRef = useRef<any>(null);
@@ -880,6 +881,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
() => resolveShortcutBinding(shortcutOptions, 'toggleQueryResultsPanel', activeShortcutPlatform),
[activeShortcutPlatform, shortcutOptions],
);
const findInEditorShortcutCombo = useMemo(
() => activeShortcutPlatform === 'mac' ? 'Meta+F' : 'Ctrl+F',
[activeShortcutPlatform],
);
const primaryShortcutModifierLabel = useMemo(
() => getShortcutPrimaryModifierDisplayLabel(activeShortcutPlatform),
[activeShortcutPlatform],
@@ -898,6 +903,25 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return nextVisible;
});
}, [tab.id, updateQueryTabDraft]);
const handleOpenEditorFind = useCallback(() => {
const editor = editorRef.current;
if (!editor) {
return;
}
editor.focus?.();
try {
const findAction = editor.getAction?.('actions.find');
if (findAction?.run) {
void findAction.run();
return;
}
} catch {
// Fall back to Monaco's built-in command id if the action lookup fails.
}
editor.trigger?.('keyboard', 'actions.find', null);
}, []);
const handleShowSqlExecutionLog = useCallback((mode: 'open' | 'toggle' = 'toggle') => {
if (!isActive) {
return;
@@ -2732,6 +2756,20 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
}
const findInEditorKeyBinding = comboToMonacoKeyBinding(
findInEditorShortcutCombo, monaco.KeyMod, monaco.KeyCode
);
if (findInEditorKeyBinding) {
findInEditorActionRef.current = editor.addAction({
id: 'gonavi.findInEditor',
label: buildQueryEditorMonacoActionLabel('query_editor.action.find_in_editor'),
keybindings: [findInEditorKeyBinding.keyMod | findInEditorKeyBinding.keyCode],
run: () => {
window.dispatchEvent(new CustomEvent('gonavi:find-active-query'));
},
});
}
const formatBinding = formatSqlShortcutBinding;
if (formatBinding?.enabled && formatBinding.combo) {
const keyBinding = comboToMonacoKeyBinding(
@@ -4998,6 +5036,40 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
}, [languagePreference, saveQueryShortcutBinding]);
useEffect(() => {
if (findInEditorActionRef.current) {
findInEditorActionRef.current.dispose();
findInEditorActionRef.current = null;
}
const editor = editorRef.current;
const monaco = monacoRef.current;
if (!editor || !monaco) return;
const keyBinding = comboToMonacoKeyBinding(
findInEditorShortcutCombo,
monaco.KeyMod,
monaco.KeyCode,
);
if (keyBinding) {
findInEditorActionRef.current = editor.addAction({
id: 'gonavi.findInEditor',
label: buildQueryEditorMonacoActionLabel('query_editor.action.find_in_editor'),
keybindings: [keyBinding.keyMod | keyBinding.keyCode],
run: () => {
window.dispatchEvent(new CustomEvent('gonavi:find-active-query'));
},
});
}
return () => {
if (findInEditorActionRef.current) {
findInEditorActionRef.current.dispose();
findInEditorActionRef.current = null;
}
};
}, [findInEditorShortcutCombo, languagePreference]);
useEffect(() => {
if (formatSqlActionRef.current) {
formatSqlActionRef.current.dispose();
@@ -5095,6 +5167,20 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
}, [isActive, handleRun, handleRunSelectedShortcut]);
useEffect(() => {
const handleFindActiveQuery = () => {
if (!isActive) {
return;
}
handleOpenEditorFind();
};
window.addEventListener('gonavi:find-active-query', handleFindActiveQuery as EventListener);
return () => {
window.removeEventListener('gonavi:find-active-query', handleFindActiveQuery as EventListener);
};
}, [handleOpenEditorFind, isActive]);
// 监听由 TabManager 分发的专用注入事件
useEffect(() => {
const handleInsertSql = (e: any) => {
@@ -5339,6 +5425,38 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
},
];
useEffect(() => {
const handleFindShortcut = (event: KeyboardEvent) => {
if (!isActive) {
return;
}
if (!isShortcutMatch(event, findInEditorShortcutCombo)) {
return;
}
const editor = editorRef.current;
const targetNode = resolveEventTargetNode(event.target);
const editorHasFocus = !!editor?.hasTextFocus?.();
const inEditorPane = !!(targetNode && editorPaneRef.current?.contains(targetNode));
const inQueryEditor = !!(targetNode && queryEditorRootRef.current?.contains(targetNode));
if (isEditableElement(event.target) && !inEditorPane) {
return;
}
if (!editorHasFocus && !inEditorPane && !inQueryEditor && !isDocumentLevelShortcutTarget(targetNode)) {
return;
}
event.preventDefault();
event.stopPropagation();
handleOpenEditorFind();
};
window.addEventListener('keydown', handleFindShortcut, true);
return () => {
window.removeEventListener('keydown', handleFindShortcut, true);
};
}, [findInEditorShortcutCombo, handleOpenEditorFind, isActive]);
useEffect(() => {
const binding = saveQueryShortcutBinding;
if (!binding?.enabled || !binding.combo) {
@@ -5605,6 +5723,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
onRun={handleRun}
onCancel={handleCancel}
onQuickSave={handleQuickSave}
onFindInEditor={handleOpenEditorFind}
onFormat={handleFormat}
onToggleResultPanelVisibility={toggleResultPanelVisibility}
onAIAction={handleAIAction}