From 6efe1797a358fef18bb8a18bc8a76454d3ffcd6d Mon Sep 17 00:00:00 2001 From: zipg Date: Tue, 7 Jul 2026 10:53:27 +0800 Subject: [PATCH] fix(editor): show gutter run button for statements with leading whitespace (#2729) Co-authored-by: zipg <4047349+zipg@users.noreply.github.com> --- .../sql/executableStatementRangeCache.spec.ts | 19 +++++++++++++++++++ .../lib/sql/executableStatementRangeCache.ts | 10 ++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/sql/executableStatementRangeCache.spec.ts b/apps/desktop/src/lib/__tests__/sql/executableStatementRangeCache.spec.ts index bd6ae1a3cc..3a0f9d7da9 100644 --- a/apps/desktop/src/lib/__tests__/sql/executableStatementRangeCache.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/executableStatementRangeCache.spec.ts @@ -27,6 +27,25 @@ describe("executableStatementRangeCacheForDoc", () => { expect(executableStatementRangeStartingAt(cache, secondStatementLine.from)?.sql).toBe("SELECT *\nFROM menus AS mn\nLIMIT 100"); }); + it("resolves statements with leading whitespace for gutter run buttons", () => { + const doc = Text.of([" SELECT 1;", " SELECT 2;", "\t SELECT 3;", "", " "]); + const cache = executableStatementRangeCacheForDoc(null, doc, "mysql"); + + expect(executableStatementRangeStartingAt(cache, doc.line(1).from)?.sql).toBe("SELECT 1"); + expect(executableStatementRangeStartingAt(cache, doc.line(2).from)?.sql).toBe("SELECT 2"); + expect(executableStatementRangeStartingAt(cache, doc.line(3).from)?.sql).toBe("SELECT 3"); + expect(executableStatementRangeStartingAt(cache, doc.line(4).from)).toBeNull(); + expect(executableStatementRangeStartingAt(cache, doc.line(5).from)).toBeNull(); + }); + + it("does not resolve gutter run buttons when non-whitespace precedes the statement on the same line", () => { + const doc = Text.of(["/* comment */ SELECT 1;"]); + const cache = executableStatementRangeCacheForDoc(null, doc, "mysql"); + + expect(executableStatementRangeStartingAt(cache, doc.line(1).from)).toBeNull(); + expect(executableStatementRangeStartingAt(cache, doc.toString().indexOf("SELECT"))?.sql).toBe("SELECT 1"); + }); + it("resolves the current statement from a cursor inside a continuation line", () => { const doc = Text.of(["SELECT *", "FROM apis AS ap", "LIMIT 100;", "", "SELECT *", "FROM menus AS mn", "LIMIT 100;"]); const cache = executableStatementRangeCacheForDoc(null, doc, "mysql"); diff --git a/apps/desktop/src/lib/sql/executableStatementRangeCache.ts b/apps/desktop/src/lib/sql/executableStatementRangeCache.ts index 3564289034..4011879437 100644 --- a/apps/desktop/src/lib/sql/executableStatementRangeCache.ts +++ b/apps/desktop/src/lib/sql/executableStatementRangeCache.ts @@ -6,6 +6,7 @@ export interface ExecutableStatementRangeCache { doc: Text; databaseType?: DatabaseType; byStart: Map; + byExecutableLineStart: Map; ranges: SqlTextRange[]; } @@ -15,15 +16,20 @@ export function executableStatementRangeCacheForDoc(cache: ExecutableStatementRa if (cache?.doc === doc && cache.databaseType === databaseType) return cache; const byStart = new Map(); + const byExecutableLineStart = new Map(); const ranges = parse(doc.toString(), databaseType); for (const range of ranges) { byStart.set(range.from, range); + const line = doc.lineAt(range.from); + if (doc.sliceString(line.from, range.from).trim() === "") { + byExecutableLineStart.set(line.from, range); + } } - return { doc, databaseType, byStart, ranges }; + return { doc, databaseType, byStart, byExecutableLineStart, ranges }; } export function executableStatementRangeStartingAt(cache: ExecutableStatementRangeCache, lineFrom: number): SqlTextRange | null { - return cache.byStart.get(lineFrom) ?? null; + return cache.byStart.get(lineFrom) ?? cache.byExecutableLineStart.get(lineFrom) ?? null; } export function executableStatementRangeAtCursor(cache: ExecutableStatementRangeCache, cursorPos: number): SqlTextRange | null {