mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-23 06:53:52 +08:00
🐛 fix(query-editor): 修复当前语句快捷选择在 CRLF 文本下错位
- 统一当前语句选择与执行路径的归一化 offset/position 换算 - 避免 Windows CRLF 文本下 SQL 语句选区错位 - 补充 QueryEditor 当前语句选择回归测试 Fixes #575
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user