From f1132bf884c5c0fc098bf1d85fae27277ab3513f Mon Sep 17 00:00:00 2001 From: blankll Date: Tue, 28 Jul 2026 17:31:30 +0800 Subject: [PATCH] fix: align SQL execute gutter icon to actual statement start line The scanner's bufStart sits right after the previous `;`, so when statements are separated by blank lines or comment lines, the gutter icon was displayed on the wrong line (the blank after `;` or the comment line) instead of the first non-whitespace, non-comment line (the actual SQL keyword). Added skipLeadingComments() to count leading whitespace, `--` line comments, and `/* */` block comments, shifting the icon to the first line with actual SQL content. Includes tests for blank-line-separated and comment-separated statements. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/composables/useSqlStatements.ts | 61 +++++++++++++++++++++- tests/composables/useSqlStatements.test.ts | 47 +++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/composables/useSqlStatements.ts b/src/composables/useSqlStatements.ts index 6dc843d2..321e2e76 100644 --- a/src/composables/useSqlStatements.ts +++ b/src/composables/useSqlStatements.ts @@ -207,13 +207,70 @@ function lineAtOffset(content: string, offset: number): number { return line } +/** + * Count leading lines in `text` that are whitespace-only or comment-only + * (`--` line comments, `/ * ... * /` block comments). Used to position the + * gutter execute icon on the first line with actual SQL content. + */ +function skipLeadingComments(text: string): number { + const lines = text.split('\n') + let inBlockComment = false + let skipped = 0 + + for (const line of lines) { + if (inBlockComment) { + const endIdx = line.indexOf('*/') + if (endIdx !== -1) { + inBlockComment = false + if (line.slice(endIdx + 2).trim().length > 0) { + // SQL after block comment on same line — stop + break + } + } + skipped++ + continue + } + + const trimmed = line.trim() + + if (trimmed.length === 0) { + skipped++ + continue + } + + if (trimmed.startsWith('--')) { + skipped++ + continue + } + + if (trimmed.startsWith('/*')) { + const endIdx = trimmed.indexOf('*/') + if (endIdx === -1) { + inBlockComment = true + skipped++ + continue + } + if (trimmed.slice(endIdx + 2).trim().length > 0) { + // SQL after inline block comment on same line + break + } + skipped++ + continue + } + + // First line with actual SQL content + break + } + + return skipped +} + export function parseSqlStatements(content: string): SqlStatement[] { const ranges = scanStatements(content) const lines = content.split('\n') return ranges .filter((r) => { - // Remove trailing semicolon for the statement text const t = r.text.trim().replace(/;\s*$/, '').trim() return t.length > 0 }) @@ -222,7 +279,7 @@ export function parseSqlStatements(content: string): SqlStatement[] { return { statement, position: { - startLineNumber: r.startLine, + startLineNumber: r.startLine + skipLeadingComments(r.text), endLineNumber: r.endLine, startColumn: 1, endColumn: (lines[r.endLine - 1]?.length ?? 1) + 1, diff --git a/tests/composables/useSqlStatements.test.ts b/tests/composables/useSqlStatements.test.ts index 61653ada..c672b038 100644 --- a/tests/composables/useSqlStatements.test.ts +++ b/tests/composables/useSqlStatements.test.ts @@ -115,6 +115,53 @@ describe('parseSqlStatements', () => { }) }) + describe('statement positions (gutter icon alignment)', () => { + it('records correct startLineNumber for second statement when blank lines separate them', () => { + const sql = [ + 'SELECT 1;', // line 1 + '', // line 2 (blank) + '', // line 3 (blank) + 'SELECT 2;', // line 4 + ].join('\n') + + const result = parseSqlStatements(sql) + expect(result).toHaveLength(2) + // First statement starts at line 1 + expect(result[0].position.startLineNumber).toBe(1) + // Second statement should start at line 4 (first non-blank line), + // NOT at the line after the ; (line 1) + expect(result[1].position.startLineNumber).toBe(4) + }) + + it('records correct startLineNumber when preceded by blank lines and comment', () => { + const sql = [ + 'SELECT 1;', // line 1 + '', // line 2 (blank) + '', // line 3 (blank) + '-- comment', // line 4 + 'SELECT 2;', // line 5 + ].join('\n') + + const result = parseSqlStatements(sql) + expect(result).toHaveLength(2) + // Icon should be at line 5 (first SQL line), not at the -- comment (line 4) + // or the blank after ; (line 1) + expect(result[1].position.startLineNumber).toBe(5) + }) + + it('keeps startLineNumber unchanged when there is no leading blank', () => { + const sql = [ + 'SELECT 1;', + 'SELECT 2;', + ].join('\n') + + const result = parseSqlStatements(sql) + expect(result).toHaveLength(2) + expect(result[0].position.startLineNumber).toBe(1) + expect(result[1].position.startLineNumber).toBe(2) + }) + }) + describe('nested parens (subqueries)', () => { it('does not split on SELECT inside a subquery', () => { const sql = [