feat(query-editor): 新增选择当前语句快捷键

- 快捷键配置:新增选择当前语句动作,默认绑定 Ctrl+E
- 编辑器接入:在 Monaco 查询编辑器中注册选择当前语句 action
- 语句识别:新增 SQL 语句范围解析,支持按光标定位当前语句
- 兼容处理:忽略字符串、注释和 dollar quote 内的分号
- 测试覆盖:补充快捷键默认配置和语句选择解析单元测试
Refs #404
This commit is contained in:
Syngnat
2026-05-14 09:19:28 +08:00
parent f8abe60dc2
commit 6456658576
6 changed files with 300 additions and 3 deletions

View File

@@ -17,6 +17,7 @@ import { isOracleLikeDialect, resolveSqlDialect, resolveSqlFunctions, resolveSql
import { applyQueryAutoLimit } from '../utils/queryAutoLimit';
import { extractQueryResultTableRef, type QueryResultTableRef } from '../utils/queryResultTable';
import { quoteIdentPart } from '../utils/sql';
import { resolveCurrentSqlStatementRange } from '../utils/sqlStatementSelection';
import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert';
import { ORACLE_ROWID_LOCATOR_COLUMN, type EditRowLocator } from '../utils/rowLocator';
@@ -637,6 +638,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const editorRef = useRef<any>(null);
const monacoRef = useRef<any>(null);
const runQueryActionRef = useRef<any>(null);
const selectCurrentStatementActionRef = useRef<any>(null);
const lastExternalQueryRef = useRef<string>(tab.query || '');
const dragRef = useRef<{ startY: number, startHeight: number } | null>(null);
const queryEditorRootRef = useRef<HTMLDivElement | null>(null);
@@ -721,6 +723,31 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return query || '';
};
const handleSelectCurrentStatement = () => {
const editor = editorRef.current;
const monaco = monacoRef.current;
const model = editor?.getModel?.();
const position = editor?.getPosition?.();
if (!editor || !monaco || !model || !position) {
return;
}
const fullSQL = String(model.getValue?.() || '');
const cursorOffset = model.getOffsetAt?.(position);
const range = resolveCurrentSqlStatementRange(fullSQL, Number(cursorOffset));
if (!range) {
void message.info('没有可选择的 SQL 语句。');
return;
}
const start = model.getPositionAt(range.start);
const end = model.getPositionAt(range.end);
const selection = new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column);
editor.setSelection(selection);
editor.revealRangeInCenterIfOutsideViewport?.(selection);
editor.focus?.();
};
const syncQueryToEditor = (sql: string) => {
const next = sql || '';
setQuery(next);
@@ -942,6 +969,21 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
}
const selectStatementBinding = shortcutOptions.selectCurrentStatement;
if (selectStatementBinding?.enabled && selectStatementBinding.combo) {
const keyBinding = comboToMonacoKeyBinding(
selectStatementBinding.combo, monaco.KeyMod, monaco.KeyCode
);
if (keyBinding) {
selectCurrentStatementActionRef.current = editor.addAction({
id: 'gonavi.selectCurrentStatement',
label: 'GoNavi: 选择当前语句',
keybindings: [keyBinding.keyMod | keyBinding.keyCode],
run: handleSelectCurrentStatement,
});
}
}
// HMR 重载时释放旧注册避免补全项重复
if (!sqlCompletionRegistered) {
sqlCompletionRegistered = true;
@@ -2226,6 +2268,37 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
}, [shortcutOptions.runQuery]);
useEffect(() => {
if (selectCurrentStatementActionRef.current) {
selectCurrentStatementActionRef.current.dispose();
selectCurrentStatementActionRef.current = null;
}
const editor = editorRef.current;
const monaco = monacoRef.current;
if (!editor || !monaco) return;
const binding = shortcutOptions.selectCurrentStatement;
if (!binding?.enabled || !binding.combo) return;
const keyBinding = comboToMonacoKeyBinding(binding.combo, monaco.KeyMod, monaco.KeyCode);
if (keyBinding) {
selectCurrentStatementActionRef.current = editor.addAction({
id: 'gonavi.selectCurrentStatement',
label: 'GoNavi: 选择当前语句',
keybindings: [keyBinding.keyMod | keyBinding.keyCode],
run: handleSelectCurrentStatement,
});
}
return () => {
if (selectCurrentStatementActionRef.current) {
selectCurrentStatementActionRef.current.dispose();
selectCurrentStatementActionRef.current = null;
}
};
}, [shortcutOptions.selectCurrentStatement]);
useEffect(() => {
const handleRunActiveQuery = () => {
if (activeTabId !== tab.id) {