fix(grid): preserve viewport during gutter execution

This commit is contained in:
dadong authored and GitHub committed 2026-08-18 10:52:13 +08:00
1 parent 99ded5c8bb
commit a6945a19f2
8 files changed
+131 -14

No files matched your search

@@ -1,8 +1,12 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { createQueryEditorExecutionViewportOwnership } from "@/lib/editor/queryEditorExecutionViewport";
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
const contentAreaSource = readFileSync(new URL("../../../components/layout/ContentArea.vue", import.meta.url), "utf8");
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
const sqlExecutionSource = readFileSync(new URL("../../../composables/useSqlExecution.ts", import.meta.url), "utf8");
const queryStoreSource = readFileSync(new URL("../../../stores/queryStore.ts", import.meta.url), "utf8");
describe("QueryEditor execution routing", () => {
it("routes the execution shortcut through the shared execution-mode contract while bypassing the picker", () => {
@@ -46,10 +50,18 @@ describe("QueryEditor execution routing", () => {
});
it("preserves the source range when executing from the statement gutter", () => {
expect(queryEditorSource).toContain("emitExecutionRequest(sqlExecutionSnapshotForRange(currentView, statementRange))");
expect(queryEditorSource).toContain("const editorViewportRequestId = gutterExecutionViewport.beginRequest()");
expect(queryEditorSource).toContain("emitExecutionRequest({ ...sqlExecutionSnapshotForRange(currentView, statementRange), editorViewportRequestId })");
expect(queryEditorSource).not.toContain('emit("execute", statementRange.sql)');
});
it("claims gutter viewport ownership only after the matching execution starts", () => {
expect(appSource).toContain("acceptQueryEditorExecutionViewport(editorViewportRequestId)");
expect(contentAreaSource).toContain("acceptGutterExecutionViewport(requestId)");
expect(sqlExecutionSource).toContain("onExecutionStarted: () => deps.onExecutionStarted?.(options.editorViewportRequestId!)");
expect(queryStoreSource.indexOf("tab.isExecuting = true")).toBeLessThan(queryStoreSource.indexOf("options?.onExecutionStarted?.()"));
});
it("lets the shortcut skip the picker without affecting other execution entry points", () => {
// The picker guard must also honor the shortcut's bypass flag, otherwise Ctrl+Enter would keep popping the dialog.
expect(queryEditorSource).toContain("if (options.bypassPicker || !settingsStore.editorSettings.showExecutionTargetPicker");
@@ -63,6 +75,41 @@ describe("QueryEditor execution routing", () => {
});
});
describe("QueryEditor gutter execution viewport ownership", () => {
it("does not let a cancelled or early-returned gutter request affect the next ordinary execution", () => {
const ownership = createQueryEditorExecutionViewportOwnership();
const cancelledRequestId = ownership.beginRequest();
ownership.cancelPendingRequest();
expect(ownership.acceptRequest(cancelledRequestId)).toBe(false);
expect(ownership.consumeAcceptedRequest()).toBe(false);
});
it("preserves the viewport once for the matching accepted execution", () => {
const ownership = createQueryEditorExecutionViewportOwnership();
const requestId = ownership.beginRequest();
expect(ownership.acceptRequest(requestId)).toBe(true);
expect(ownership.consumeAcceptedRequest()).toBe(true);
expect(ownership.consumeAcceptedRequest()).toBe(false);
});
it("clears pending and accepted ownership when the editor becomes inactive", () => {
const ownership = createQueryEditorExecutionViewportOwnership();
const pendingRequestId = ownership.beginRequest();
ownership.reset();
expect(ownership.acceptRequest(pendingRequestId)).toBe(false);
const acceptedRequestId = ownership.beginRequest();
expect(ownership.acceptRequest(acceptedRequestId)).toBe(true);
ownership.reset();
expect(ownership.consumeAcceptedRequest()).toBe(false);
});
});
describe("ContentArea execution summary errors", () => {
it("keeps batch errors selectable and copyable without triggering statement navigation", () => {
expect(contentAreaSource).toContain('class="absolute inset-0 z-0 cursor-pointer');
@@ -0,0 +1,31 @@
export function createQueryEditorExecutionViewportOwnership() {
let nextRequestId = 0;
let pendingRequestId: number | undefined;
let acceptedRequestId: number | undefined;
return {
beginRequest(): number {
nextRequestId += 1;
pendingRequestId = nextRequestId;
return nextRequestId;
},
cancelPendingRequest() {
pendingRequestId = undefined;
},
acceptRequest(requestId: number): boolean {
if (pendingRequestId !== requestId) return false;
pendingRequestId = undefined;
acceptedRequestId = requestId;
return true;
},
consumeAcceptedRequest(): boolean {
if (acceptedRequestId === undefined) return false;
acceptedRequestId = undefined;
return true;
},
reset() {
pendingRequestId = undefined;
acceptedRequestId = undefined;
},
};
}
@@ -10,6 +10,7 @@ export interface SqlExecutionSnapshot {
cursorPos: number;
selectionFrom: number;
selectionTo: number;
editorViewportRequestId?: number;
}
export type SqlExecutionOverride = string | SqlExecutionSnapshot;