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

@@ -35,6 +35,12 @@ describe('QueryEditor i18n source guards', () => {
expect(queryEditorSource).not.toContain('翻页未返回结果集');
});
it('routes editor search through a localized Monaco action', () => {
expect(queryEditorSource).toContain('query_editor.action.find_in_editor');
expect(queryEditorSource).toContain('gonavi:find-active-query');
expect(queryEditorSource).toContain("editor.getAction?.('actions.find')");
});
it('uses a localized wrapper for save query failures', () => {
expect(queryEditorSource).toContain('query_editor.message.save_query_failed');
expect(queryEditorSource).not.toContain('保存查询失败: ');

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}

View File

@@ -42,6 +42,8 @@ const requiredKeys = [
'query_editor.action.save',
'query_editor.action.save_with_shortcut',
'query_editor.action.more',
'query_editor.action.find_in_editor',
'query_editor.action.find_in_editor_with_shortcut',
'query_editor.action.format',
'query_editor.action.format_sql',
'query_editor.action.format_sql_with_shortcut',

View File

@@ -80,6 +80,19 @@ describe('QueryEditorToolbar layout', () => {
expect(transactionDelayCss).toContain('flex: 0 0 104px !important;');
});
it('keeps editor search action grouped with formatting actions', () => {
const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');
const formatActionPairStart = toolbarSource.indexOf('SearchOutlined');
const formatActionPairEnd = toolbarSource.indexOf('!isV2Ui && (');
const formatActionPairSource = toolbarSource.slice(formatActionPairStart, formatActionPairEnd);
expect(toolbarSource).toContain('SearchOutlined');
expect(formatActionPairSource).toContain('query_editor.action.find_in_editor');
expect(formatActionPairSource).toContain('query_editor.action.find_in_editor_with_shortcut');
expect(formatActionPairSource).toContain('onClick={onFindInEditor}');
expect(formatActionPairSource).toContain('FormatPainterOutlined');
});
it('shows delayed full-name tooltips for truncated connection and database selectors', () => {
const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');
const css = readV2ThemeCss();

View File

@@ -6,6 +6,7 @@ import {
FormatPainterOutlined,
PlayCircleOutlined,
RobotOutlined,
SearchOutlined,
SaveOutlined,
SettingOutlined,
StopOutlined,
@@ -51,6 +52,7 @@ type QueryEditorToolbarProps = {
onRun: () => void;
onCancel: () => void;
onQuickSave: () => void;
onFindInEditor: () => void;
onFormat: () => void;
onToggleResultPanelVisibility: () => void;
onAIAction: (action: "generate" | "explain" | "optimize" | "schema") => void;
@@ -112,6 +114,7 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
onRun,
onCancel,
onQuickSave,
onFindInEditor,
onFormat,
onToggleResultPanelVisibility,
onAIAction,
@@ -161,6 +164,17 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
),
})
: t("query_editor.action.format_sql");
const findInEditorShortcutCombo =
activeShortcutPlatform === "mac" ? "Meta+F" : "Ctrl+F";
const findInEditorTitle = t(
"query_editor.action.find_in_editor_with_shortcut",
{
shortcut: getShortcutDisplayLabel(
findInEditorShortcutCombo,
activeShortcutPlatform,
),
},
);
const aiMenuItems: MenuProps["items"] = [
{
key: "ai-generate",
@@ -371,6 +385,11 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
className={isV2Ui ? "gn-v2-query-toolbar-action-pair" : undefined}
style={{ display: "flex", gap: "8px", alignItems: "center" }}
>
<Tooltip title={findInEditorTitle}>
<Button icon={<SearchOutlined />} onClick={onFindInEditor}>
{t("query_editor.action.find_in_editor")}
</Button>
</Tooltip>
<Tooltip title={formatSqlTitle}>
<Button icon={<FormatPainterOutlined />} onClick={onFormat}>
{t("query_editor.action.format")}

View File

@@ -5940,6 +5940,8 @@
"query_editor.action.ai_schema_analysis": "Schema-Analyse",
"query_editor.action.export_sql_file": "SQL-Datei exportieren",
"query_editor.action.format": "Formatieren",
"query_editor.action.find_in_editor": "Suchen",
"query_editor.action.find_in_editor_with_shortcut": "Im Editor suchen ({{shortcut}})",
"query_editor.action.format_sql": "SQL formatieren",
"query_editor.action.format_sql_with_shortcut": "SQL formatieren ({{shortcut}})",
"query_editor.action.hide_results_panel": "Ergebnisbereich ausblenden",

View File

@@ -5940,6 +5940,8 @@
"query_editor.action.ai_schema_analysis": "Schema analysis",
"query_editor.action.export_sql_file": "Export SQL file",
"query_editor.action.format": "Format",
"query_editor.action.find_in_editor": "Find",
"query_editor.action.find_in_editor_with_shortcut": "Find in editor ({{shortcut}})",
"query_editor.action.format_sql": "Format SQL",
"query_editor.action.format_sql_with_shortcut": "Format SQL ({{shortcut}})",
"query_editor.action.hide_results_panel": "Hide results panel",

View File

@@ -5940,6 +5940,8 @@
"query_editor.action.ai_schema_analysis": "スキーマ分析",
"query_editor.action.export_sql_file": "SQL ファイルをエクスポート",
"query_editor.action.format": "整形",
"query_editor.action.find_in_editor": "検索",
"query_editor.action.find_in_editor_with_shortcut": "エディター内を検索({{shortcut}}",
"query_editor.action.format_sql": "SQL を整形",
"query_editor.action.format_sql_with_shortcut": "SQL を整形({{shortcut}}",
"query_editor.action.hide_results_panel": "結果エリアを非表示",

View File

@@ -5940,6 +5940,8 @@
"query_editor.action.ai_schema_analysis": "Анализ схемы",
"query_editor.action.export_sql_file": "Экспортировать SQL-файл",
"query_editor.action.format": "Форматировать",
"query_editor.action.find_in_editor": "Поиск",
"query_editor.action.find_in_editor_with_shortcut": "Поиск в редакторе ({{shortcut}})",
"query_editor.action.format_sql": "Форматировать SQL",
"query_editor.action.format_sql_with_shortcut": "Форматировать SQL ({{shortcut}})",
"query_editor.action.hide_results_panel": "Скрыть область результатов",

View File

@@ -5940,6 +5940,8 @@
"query_editor.action.ai_schema_analysis": "Schema 分析",
"query_editor.action.export_sql_file": "导出 SQL 文件",
"query_editor.action.format": "美化",
"query_editor.action.find_in_editor": "搜索",
"query_editor.action.find_in_editor_with_shortcut": "编辑器内搜索({{shortcut}}",
"query_editor.action.format_sql": "美化 SQL",
"query_editor.action.format_sql_with_shortcut": "美化 SQL{{shortcut}}",
"query_editor.action.hide_results_panel": "隐藏结果区",

View File

@@ -5940,6 +5940,8 @@
"query_editor.action.ai_schema_analysis": "結構描述分析",
"query_editor.action.export_sql_file": "匯出 SQL 檔案",
"query_editor.action.format": "格式化",
"query_editor.action.find_in_editor": "搜尋",
"query_editor.action.find_in_editor_with_shortcut": "編輯器內搜尋({{shortcut}}",
"query_editor.action.format_sql": "格式化 SQL",
"query_editor.action.format_sql_with_shortcut": "格式化 SQL{{shortcut}}",
"query_editor.action.hide_results_panel": "隱藏結果區",