From ae86a8c73c40cc67fa9eeae7c152fd5055130c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=88=E7=A7=8B?= <49514152+yiqiui@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:57:10 +0800 Subject: [PATCH] fix(editor): keep viewport when cursor was visible before execution --- .../src/components/editor/QueryEditor.vue | 7 ++++- .../editor/queryEditorExecution.spec.ts | 26 +++++++++++++++++++ .../editor/queryEditorExecutionViewport.ts | 12 +++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 456ac4f556..f55338f55f 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -2754,7 +2754,12 @@ function scrollCursorIntoView() { } function beginExecutionViewportTracking() { - executionViewportOwnership.beginExecution(); + // Snapshot visibility while the viewport still has its pre-execution size: + // once the results pane opens it shrinks the editor, and a cursor that was + // visible before execution must not be scrolled away by the shrink (#10480). + const currentView = view.value; + const cursorVisible = currentView ? isQueryEditorPositionVisible(currentView.state.selection.main.head, currentView.visibleRanges, currentView.viewport) : false; + executionViewportOwnership.beginExecution(cursorVisible); } function recordExecutionViewportInteraction() { diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorExecution.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorExecution.spec.ts index c4574cb08d..913b436368 100644 --- a/apps/desktop/src/lib/__tests__/editor/queryEditorExecution.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorExecution.spec.ts @@ -9,6 +9,10 @@ describe("QueryEditor execution routing", () => { expect(queryEditorSource).toContain("createQueryEditorExecutionShortcutBindings(shortcuts.executeSql"); expect(queryEditorSource).not.toContain("forceCurrent"); }); + + it("snapshots pre-execution cursor visibility when execution viewport tracking starts", () => { + expect(queryEditorSource).toContain("beginExecution(cursorVisible)"); + }); }); describe("QueryEditor execution viewport ownership", () => { @@ -20,6 +24,28 @@ describe("QueryEditor execution viewport ownership", () => { expect(ownership.consumeCompletionPreservation()).toBe(false); }); + it("preserves the viewport when the cursor was visible before execution (#10480)", () => { + const ownership = createQueryEditorExecutionViewportOwnership(); + + // Cmd+Enter path: no gutter request, no user interaction, but the cursor + // was comfortably visible in the pre-execution viewport. The results pane + // then shrinks the editor — that shrink must not scroll the cursor away. + ownership.beginExecution(true); + + expect(ownership.consumeCompletionPreservation()).toBe(true); + expect(ownership.consumeCompletionPreservation()).toBe(false); + }); + + it("still centers the cursor when it was off-screen before execution", () => { + const ownership = createQueryEditorExecutionViewportOwnership(); + + // #5281 contract: a cursor that was already out of sight before execution + // gets centered once the results pane has taken its space. + ownership.beginExecution(false); + + expect(ownership.consumeCompletionPreservation()).toBe(false); + }); + it("preserves the viewport once after user interaction during execution", () => { const ownership = createQueryEditorExecutionViewportOwnership(); diff --git a/apps/desktop/src/lib/editor/queryEditorExecutionViewport.ts b/apps/desktop/src/lib/editor/queryEditorExecutionViewport.ts index 5dc9c02caa..da061cdda8 100644 --- a/apps/desktop/src/lib/editor/queryEditorExecutionViewport.ts +++ b/apps/desktop/src/lib/editor/queryEditorExecutionViewport.ts @@ -14,6 +14,7 @@ export function createQueryEditorExecutionViewportOwnership() { let acceptedRequestId: number | undefined; let executionActive = false; let userInteractedDuringExecution = false; + let cursorVisibleBeforeExecution = false; return { beginRequest(): number { @@ -33,18 +34,24 @@ export function createQueryEditorExecutionViewportOwnership() { acceptedRequestId = requestId; return true; }, - beginExecution() { + // Captured while the editor viewport still has its pre-execution size: once + // the results pane opens it shrinks the editor, and a cursor that was + // comfortably visible before then can fall outside the smaller viewport, + // which must not be read as "the user cannot see their cursor" (#10480). + beginExecution(cursorVisible = false) { executionActive = true; userInteractedDuringExecution = false; + cursorVisibleBeforeExecution = cursorVisible; }, recordUserInteraction() { if (executionActive) userInteractedDuringExecution = true; }, consumeCompletionPreservation(): boolean { - const preserveViewport = acceptedRequestId !== undefined || userInteractedDuringExecution; + const preserveViewport = acceptedRequestId !== undefined || userInteractedDuringExecution || cursorVisibleBeforeExecution; acceptedRequestId = undefined; executionActive = false; userInteractedDuringExecution = false; + cursorVisibleBeforeExecution = false; return preserveViewport; }, reset() { @@ -52,6 +59,7 @@ export function createQueryEditorExecutionViewportOwnership() { acceptedRequestId = undefined; executionActive = false; userInteractedDuringExecution = false; + cursorVisibleBeforeExecution = false; }, }; }