🐛 fix(query-editor): 修复当前语句快捷选择在 CRLF 文本下错位

- 统一当前语句选择与执行路径的归一化 offset/position 换算
- 避免 Windows CRLF 文本下 SQL 语句选区错位
- 补充 QueryEditor 当前语句选择回归测试

Fixes #575
This commit is contained in:
Syngnat
2026-06-21 15:08:34 +08:00
parent 29e7e365f1
commit e7b8e78f9c
3 changed files with 58 additions and 4 deletions

View File

@@ -224,6 +224,9 @@ const editorState = vi.hoisted(() => {
setSelection: vi.fn((selection: any) => {
state.selection = selection;
}),
setSelections: vi.fn((selections: any[]) => {
state.selection = Array.isArray(selections) ? selections[0] ?? null : null;
}),
executeEdits: vi.fn((_source: string, edits: any[]) => {
edits.forEach((edit) => {
const start = offsetAt({ lineNumber: edit.range.startLineNumber, column: edit.range.startColumn });
@@ -2270,6 +2273,37 @@ describe('QueryEditor external SQL save', () => {
expect(messageApi.info).not.toHaveBeenCalledWith('没有可选择的 SQL 语句。');
});
it('selects only the current SQL statement when the editor content uses CRLF line endings', async () => {
storeState.shortcutOptions.selectCurrentStatement.windows = { enabled: true, combo: 'Ctrl+Q' };
const sql = [
'SELECT * FROM first_table;',
'',
'SELECT * FROM second_table;',
'',
'SELECT a.id, a.name FROM third_table a ORDER BY a.id;',
].join('\r\n');
editorState.position = { lineNumber: 5, column: 18 };
editorState.selection = null;
await act(async () => {
create(<QueryEditor tab={createTab({ query: sql, readOnly: true })} />);
});
const selectCurrentStatementAction = findEditorAction('gonavi.selectCurrentStatement');
expect(selectCurrentStatementAction).toBeTruthy();
await act(async () => {
await selectCurrentStatementAction.run();
});
expect(editorState.selection).toMatchObject({
startLineNumber: 5,
startColumn: 1,
endLineNumber: 5,
endColumn: 'SELECT a.id, a.name FROM third_table a ORDER BY a.id;'.length,
});
});
it('shows the object info miss toast in English when the cursor is not on a recognized table or column', async () => {
storeState.languagePreference = 'en-US';
setCurrentLanguage('en-US');

View File

@@ -85,6 +85,7 @@ import {
dispatchQueryEditorSidebarLocate,
getCaseInsensitiveValue,
getFirstRowValue,
getNormalizedPositionAtOffset,
getInitialEditorQuery,
getMySQLShowTablesName,
getNormalizedOffsetAtPosition,
@@ -752,16 +753,21 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
const fullSQL = String(model.getValue?.() || '');
const cursorOffset = model.getOffsetAt?.(position);
const range = resolveCurrentSqlStatementRange(fullSQL, Number(cursorOffset));
const normalizedPosition = normalizeEditorPosition(position);
if (!normalizedPosition) {
return;
}
const cursorOffset = getNormalizedOffsetAtPosition(fullSQL, normalizedPosition);
const range = resolveCurrentSqlStatementRange(fullSQL, cursorOffset);
if (!range) {
void message.info(translate('query_editor.message.no_selectable_sql'));
return;
}
const start = model.getPositionAt(range.start);
const end = model.getPositionAt(range.end);
const start = getNormalizedPositionAtOffset(fullSQL, range.start);
const end = getNormalizedPositionAtOffset(fullSQL, range.end);
const selection = new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column);
editor.setSelections?.([selection]);
editor.setSelection(selection);
editor.revealRangeInCenterIfOutsideViewport?.(selection);
editor.focus?.();

View File

@@ -604,6 +604,20 @@ export const getNormalizedOffsetAtPosition = (
return Math.max(0, Math.min(text.length, offset + Math.max(0, position.column - 1)));
};
export const getNormalizedPositionAtOffset = (
sqlText: string,
offset: number,
): { lineNumber: number; column: number } => {
const text = String(sqlText || '').replace(/\r\n/g, '\n');
const safeOffset = Math.max(0, Math.min(text.length, Number.isFinite(offset) ? Math.trunc(offset) : 0));
const prefix = text.slice(0, safeOffset);
const lines = prefix.split('\n');
return {
lineNumber: Math.max(1, lines.length),
column: (lines[lines.length - 1]?.length || 0) + 1,
};
};
export const getFirstRowValue = (row: Record<string, any>): string => {
for (const value of Object.values(row || {})) {
if (value !== undefined && value !== null) {