diff --git a/src/composables/useSqlStatements.ts b/src/composables/useSqlStatements.ts index 6dc843d..321e2e7 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 61653ad..c672b03 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 = [