From e7b8e78f9c98fb89e5c19a1dbdea91d04bb388a9 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sun, 21 Jun 2026 15:08:34 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=BD=93=E5=89=8D=E8=AF=AD=E5=8F=A5=E5=BF=AB=E6=8D=B7?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=9C=A8=20CRLF=20=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E4=B8=8B=E9=94=99=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一当前语句选择与执行路径的归一化 offset/position 换算 - 避免 Windows CRLF 文本下 SQL 语句选区错位 - 补充 QueryEditor 当前语句选择回归测试 Fixes #575 --- .../QueryEditor.external-sql-save.test.tsx | 34 +++++++++++++++++++ frontend/src/components/QueryEditor.tsx | 14 +++++--- .../queryEditor/QueryEditorHelpers.ts | 14 ++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 3725c0d..50e7f49 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -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(); + }); + + 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'); diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index f9704a3..5820dfa 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -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?.(); diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.ts index b913840..a0c4444 100644 --- a/frontend/src/components/queryEditor/QueryEditorHelpers.ts +++ b/frontend/src/components/queryEditor/QueryEditorHelpers.ts @@ -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 => { for (const value of Object.values(row || {})) { if (value !== undefined && value !== null) {