mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-07 11:01:33 +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) => {
|
setSelection: vi.fn((selection: any) => {
|
||||||
state.selection = selection;
|
state.selection = selection;
|
||||||
}),
|
}),
|
||||||
|
setSelections: vi.fn((selections: any[]) => {
|
||||||
|
state.selection = Array.isArray(selections) ? selections[0] ?? null : null;
|
||||||
|
}),
|
||||||
executeEdits: vi.fn((_source: string, edits: any[]) => {
|
executeEdits: vi.fn((_source: string, edits: any[]) => {
|
||||||
edits.forEach((edit) => {
|
edits.forEach((edit) => {
|
||||||
const start = offsetAt({ lineNumber: edit.range.startLineNumber, column: edit.range.startColumn });
|
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 语句。');
|
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 () => {
|
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';
|
storeState.languagePreference = 'en-US';
|
||||||
setCurrentLanguage('en-US');
|
setCurrentLanguage('en-US');
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import {
|
|||||||
dispatchQueryEditorSidebarLocate,
|
dispatchQueryEditorSidebarLocate,
|
||||||
getCaseInsensitiveValue,
|
getCaseInsensitiveValue,
|
||||||
getFirstRowValue,
|
getFirstRowValue,
|
||||||
|
getNormalizedPositionAtOffset,
|
||||||
getInitialEditorQuery,
|
getInitialEditorQuery,
|
||||||
getMySQLShowTablesName,
|
getMySQLShowTablesName,
|
||||||
getNormalizedOffsetAtPosition,
|
getNormalizedOffsetAtPosition,
|
||||||
@@ -752,16 +753,21 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fullSQL = String(model.getValue?.() || '');
|
const fullSQL = String(model.getValue?.() || '');
|
||||||
const cursorOffset = model.getOffsetAt?.(position);
|
const normalizedPosition = normalizeEditorPosition(position);
|
||||||
const range = resolveCurrentSqlStatementRange(fullSQL, Number(cursorOffset));
|
if (!normalizedPosition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cursorOffset = getNormalizedOffsetAtPosition(fullSQL, normalizedPosition);
|
||||||
|
const range = resolveCurrentSqlStatementRange(fullSQL, cursorOffset);
|
||||||
if (!range) {
|
if (!range) {
|
||||||
void message.info(translate('query_editor.message.no_selectable_sql'));
|
void message.info(translate('query_editor.message.no_selectable_sql'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = model.getPositionAt(range.start);
|
const start = getNormalizedPositionAtOffset(fullSQL, range.start);
|
||||||
const end = model.getPositionAt(range.end);
|
const end = getNormalizedPositionAtOffset(fullSQL, range.end);
|
||||||
const selection = new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column);
|
const selection = new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column);
|
||||||
|
editor.setSelections?.([selection]);
|
||||||
editor.setSelection(selection);
|
editor.setSelection(selection);
|
||||||
editor.revealRangeInCenterIfOutsideViewport?.(selection);
|
editor.revealRangeInCenterIfOutsideViewport?.(selection);
|
||||||
editor.focus?.();
|
editor.focus?.();
|
||||||
|
|||||||
@@ -604,6 +604,20 @@ export const getNormalizedOffsetAtPosition = (
|
|||||||
return Math.max(0, Math.min(text.length, offset + Math.max(0, position.column - 1)));
|
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 => {
|
export const getFirstRowValue = (row: Record<string, any>): string => {
|
||||||
for (const value of Object.values(row || {})) {
|
for (const value of Object.values(row || {})) {
|
||||||
if (value !== undefined && value !== null) {
|
if (value !== undefined && value !== null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user