fix(editor): preserve visible cursor after query execution
Closes #7037
This commit is contained in:
1 parent
9af98c2db2
commit
3232195e1f
3 files changed
+59
-6
No files matched your search
@@ -24,7 +24,7 @@ import { canFormatSqlForDatabaseType, formatSqlForEditing, compressSqlText, type
|
||||
import { detectAndFormatStructured } from "@/lib/sql/autoFormat";
|
||||
import { enabledSqlParameterSyntaxes, resolveSqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax";
|
||||
import { blankLineDeletionChanges, replaceSelectedEditorText } from "@/lib/editor/queryEditorTextEdits";
|
||||
import { createQueryEditorExecutionViewportOwnership } from "@/lib/editor/queryEditorExecutionViewport";
|
||||
import { createQueryEditorExecutionViewportOwnership, isQueryEditorPositionVisible } from "@/lib/editor/queryEditorExecutionViewport";
|
||||
import { joinQueryEditorLines } from "@/lib/editor/queryEditorJoinLines";
|
||||
import { insertQueryEditorNewline } from "@/lib/editor/queryEditorNewline";
|
||||
import { createSqlSignatureTooltipDom } from "@/lib/editor/sqlSignatureTooltip";
|
||||
@@ -6018,15 +6018,17 @@ function openReplace(): boolean {
|
||||
|
||||
function scrollCursorIntoView() {
|
||||
const preserveViewport = executionViewportOwnership.consumeCompletionPreservation();
|
||||
if (!view.value || !editorViewModule || !editorIsActive || preserveViewport) return;
|
||||
const pos = view.value.state.selection.main.head;
|
||||
const currentView = view.value;
|
||||
if (!currentView || !editorViewModule || !editorIsActive || preserveViewport) return;
|
||||
const pos = currentView.state.selection.main.head;
|
||||
if (isQueryEditorPositionVisible(pos, currentView.visibleRanges, currentView.viewport)) return;
|
||||
// Use "center" rather than "nearest": by the time this runs, the results pane has already
|
||||
// opened/resized and shrunk the editor viewport, so the cursor's old position is often no
|
||||
// longer visible. "nearest" then pins it right at the new viewport's edge (Fixes #5281: in a
|
||||
// long multi-statement file, the just-executed statement lands flush against the results pane
|
||||
// divider), which is exactly where it's hardest to see and re-click. Centering keeps it
|
||||
// comfortably visible so the user doesn't have to scroll to find/re-run it.
|
||||
view.value.dispatch({
|
||||
currentView.dispatch({
|
||||
effects: editorViewModule.EditorView.scrollIntoView(pos, { y: "center" }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createQueryEditorExecutionViewportOwnership } from "@/lib/editor/queryEditorExecutionViewport";
|
||||
import { createQueryEditorExecutionViewportOwnership, isQueryEditorPositionVisible } 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");
|
||||
@@ -83,7 +83,7 @@ describe("QueryEditor execution routing", () => {
|
||||
});
|
||||
|
||||
describe("QueryEditor execution viewport ownership", () => {
|
||||
it("keeps automatic cursor centering when the user does not interact during execution", () => {
|
||||
it("leaves completion positioning unclaimed when the user does not interact during execution", () => {
|
||||
const ownership = createQueryEditorExecutionViewportOwnership();
|
||||
|
||||
ownership.beginExecution();
|
||||
@@ -154,6 +154,47 @@ describe("QueryEditor execution viewport ownership", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("QueryEditor completion cursor visibility", () => {
|
||||
const viewport = { from: 10, to: 20 };
|
||||
|
||||
it("treats a position inside a visible range as visible", () => {
|
||||
expect(isQueryEditorPositionVisible(15, [{ from: 10, to: 20 }], viewport)).toBe(true);
|
||||
});
|
||||
|
||||
it("includes range endpoints but excludes adjacent positions", () => {
|
||||
expect(isQueryEditorPositionVisible(10, [{ from: 10, to: 20 }], viewport)).toBe(true);
|
||||
expect(isQueryEditorPositionVisible(20, [{ from: 10, to: 20 }], viewport)).toBe(true);
|
||||
expect(isQueryEditorPositionVisible(9, [{ from: 10, to: 20 }], viewport)).toBe(false);
|
||||
expect(isQueryEditorPositionVisible(21, [{ from: 10, to: 20 }], viewport)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts any visible range without treating a folded gap as visible", () => {
|
||||
const visibleRanges = [
|
||||
{ from: 10, to: 14 },
|
||||
{ from: 17, to: 20 },
|
||||
];
|
||||
|
||||
expect(isQueryEditorPositionVisible(18, visibleRanges, viewport)).toBe(true);
|
||||
expect(isQueryEditorPositionVisible(15, visibleRanges, viewport)).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to the viewport when visible ranges are unavailable or empty", () => {
|
||||
expect(isQueryEditorPositionVisible(15, undefined, viewport)).toBe(true);
|
||||
expect(isQueryEditorPositionVisible(15, [], viewport)).toBe(true);
|
||||
expect(isQueryEditorPositionVisible(21, undefined, viewport)).toBe(false);
|
||||
});
|
||||
|
||||
it("checks visibility after completion ownership and before centering", () => {
|
||||
const ownershipCheck = queryEditorSource.indexOf("executionViewportOwnership.consumeCompletionPreservation()");
|
||||
const visibilityCheck = queryEditorSource.indexOf("if (isQueryEditorPositionVisible(pos, currentView.visibleRanges, currentView.viewport)) return");
|
||||
const centerScroll = queryEditorSource.indexOf('EditorView.scrollIntoView(pos, { y: "center" })');
|
||||
|
||||
expect(ownershipCheck).toBeGreaterThan(-1);
|
||||
expect(visibilityCheck).toBeGreaterThan(ownershipCheck);
|
||||
expect(centerScroll).toBeGreaterThan(visibilityCheck);
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
export interface QueryEditorViewportRange {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
export function isQueryEditorPositionVisible(position: number, visibleRanges: readonly QueryEditorViewportRange[] | undefined, viewport: QueryEditorViewportRange): boolean {
|
||||
const ranges = visibleRanges && visibleRanges.length > 0 ? visibleRanges : [viewport];
|
||||
return ranges.some((range) => position >= range.from && position <= range.to);
|
||||
}
|
||||
|
||||
export function createQueryEditorExecutionViewportOwnership() {
|
||||
let nextRequestId = 0;
|
||||
let pendingRequestId: number | undefined;
|
||||
|
||||
Reference in new issue
Block a user