fix(editor): preserve user-adjusted SQL editor position during execution

This commit is contained in:
zipg authored and GitHub committed 2026-08-24 13:30:50 +08:00
1 parent 4d7d4ab74e
commit 8cc924270c
4 files changed
+86 -17

No files matched your search

@@ -50,7 +50,7 @@ describe("QueryEditor execution routing", () => {
});
it("preserves the source range when executing from the statement gutter", () => {
expect(queryEditorSource).toContain("const editorViewportRequestId = gutterExecutionViewport.beginRequest()");
expect(queryEditorSource).toContain("const editorViewportRequestId = executionViewportOwnership.beginRequest()");
expect(queryEditorSource).toContain("emitExecutionRequest({ ...sqlExecutionSnapshotForRange(currentView, statementRange), editorViewportRequestId })");
expect(queryEditorSource).not.toContain('emit("execute", statementRange.sql)');
});
@@ -62,6 +62,13 @@ describe("QueryEditor execution routing", () => {
expect(queryStoreSource.indexOf("tab.isExecuting = true")).toBeLessThan(queryStoreSource.indexOf("options?.onExecutionStarted?.()"));
});
it("tracks editor interaction while a query is executing", () => {
expect(contentAreaSource).toContain("queryEditorRef.value?.beginExecutionViewportTracking()");
expect(queryEditorSource).toContain('@wheel="recordExecutionViewportInteraction"');
expect(queryEditorSource).toContain('@pointerdown="recordExecutionViewportInteraction"');
expect(queryEditorSource).toContain("executionViewportOwnership.recordUserInteraction()");
});
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");
@@ -75,7 +82,33 @@ describe("QueryEditor execution routing", () => {
});
});
describe("QueryEditor gutter execution viewport ownership", () => {
describe("QueryEditor execution viewport ownership", () => {
it("keeps automatic cursor centering when the user does not interact during execution", () => {
const ownership = createQueryEditorExecutionViewportOwnership();
ownership.beginExecution();
expect(ownership.consumeCompletionPreservation()).toBe(false);
});
it("preserves the viewport once after user interaction during execution", () => {
const ownership = createQueryEditorExecutionViewportOwnership();
ownership.beginExecution();
ownership.recordUserInteraction();
expect(ownership.consumeCompletionPreservation()).toBe(true);
expect(ownership.consumeCompletionPreservation()).toBe(false);
});
it("ignores editor interaction outside an active execution", () => {
const ownership = createQueryEditorExecutionViewportOwnership();
ownership.recordUserInteraction();
expect(ownership.consumeCompletionPreservation()).toBe(false);
});
it("does not let a cancelled or early-returned gutter request affect the next ordinary execution", () => {
const ownership = createQueryEditorExecutionViewportOwnership();
const cancelledRequestId = ownership.beginRequest();
@@ -83,7 +116,7 @@ describe("QueryEditor gutter execution viewport ownership", () => {
ownership.cancelPendingRequest();
expect(ownership.acceptRequest(cancelledRequestId)).toBe(false);
expect(ownership.consumeAcceptedRequest()).toBe(false);
expect(ownership.consumeCompletionPreservation()).toBe(false);
});
it("preserves the viewport once for the matching accepted execution", () => {
@@ -91,8 +124,9 @@ describe("QueryEditor gutter execution viewport ownership", () => {
const requestId = ownership.beginRequest();
expect(ownership.acceptRequest(requestId)).toBe(true);
expect(ownership.consumeAcceptedRequest()).toBe(true);
expect(ownership.consumeAcceptedRequest()).toBe(false);
ownership.beginExecution();
expect(ownership.consumeCompletionPreservation()).toBe(true);
expect(ownership.consumeCompletionPreservation()).toBe(false);
});
it("clears pending and accepted ownership when the editor becomes inactive", () => {
@@ -106,7 +140,17 @@ describe("QueryEditor gutter execution viewport ownership", () => {
expect(ownership.acceptRequest(acceptedRequestId)).toBe(true);
ownership.reset();
expect(ownership.consumeAcceptedRequest()).toBe(false);
expect(ownership.consumeCompletionPreservation()).toBe(false);
});
it("clears execution interaction when the editor becomes inactive", () => {
const ownership = createQueryEditorExecutionViewportOwnership();
ownership.beginExecution();
ownership.recordUserInteraction();
ownership.reset();
expect(ownership.consumeCompletionPreservation()).toBe(false);
});
});
@@ -2,6 +2,8 @@ export function createQueryEditorExecutionViewportOwnership() {
let nextRequestId = 0;
let pendingRequestId: number | undefined;
let acceptedRequestId: number | undefined;
let executionActive = false;
let userInteractedDuringExecution = false;
return {
beginRequest(): number {
@@ -18,14 +20,25 @@ export function createQueryEditorExecutionViewportOwnership() {
acceptedRequestId = requestId;
return true;
},
consumeAcceptedRequest(): boolean {
if (acceptedRequestId === undefined) return false;
beginExecution() {
executionActive = true;
userInteractedDuringExecution = false;
},
recordUserInteraction() {
if (executionActive) userInteractedDuringExecution = true;
},
consumeCompletionPreservation(): boolean {
const preserveViewport = acceptedRequestId !== undefined || userInteractedDuringExecution;
acceptedRequestId = undefined;
return true;
executionActive = false;
userInteractedDuringExecution = false;
return preserveViewport;
},
reset() {
pendingRequestId = undefined;
acceptedRequestId = undefined;
executionActive = false;
userInteractedDuringExecution = false;
},
};
}