Fix initial terminal resize for nvim
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import type { Page } from "@playwright/test";
|
import type { Page } from "@playwright/test";
|
||||||
import { test, expect } from "./fixtures";
|
import { test, expect } from "./fixtures";
|
||||||
import { TerminalE2EHarness, withTerminalInApp } from "./helpers/terminal-dsl";
|
import { TerminalE2EHarness, withTerminalInApp } from "./helpers/terminal-dsl";
|
||||||
|
import { getTabTestIds } from "./helpers/launcher";
|
||||||
import {
|
import {
|
||||||
installTerminalRenderProbe,
|
installTerminalRenderProbe,
|
||||||
readTerminalRenderProbe,
|
readTerminalRenderProbe,
|
||||||
@@ -10,6 +11,54 @@ import {
|
|||||||
} from "./helpers/terminal-probes";
|
} from "./helpers/terminal-probes";
|
||||||
import { getTerminalBufferText, waitForTerminalContent } from "./helpers/terminal-perf";
|
import { getTerminalBufferText, waitForTerminalContent } from "./helpers/terminal-perf";
|
||||||
|
|
||||||
|
interface TerminalLayoutMetrics {
|
||||||
|
visibleSurfaceCount: number;
|
||||||
|
surfaceHeight: number;
|
||||||
|
rowCount: number;
|
||||||
|
rows: number | null;
|
||||||
|
cols: number | null;
|
||||||
|
renderedRowsHeight: number;
|
||||||
|
tabIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readTerminalLayoutMetrics(page: Page): Promise<TerminalLayoutMetrics> {
|
||||||
|
const tabIds = await getTabTestIds(page);
|
||||||
|
return page.evaluate((currentTabIds) => {
|
||||||
|
const visibleSurfaces = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>('[data-testid="terminal-surface"]'),
|
||||||
|
).filter((candidate) => {
|
||||||
|
const rect = candidate.getBoundingClientRect();
|
||||||
|
return rect.width > 0 && rect.height > 0;
|
||||||
|
});
|
||||||
|
const surface = visibleSurfaces[0] ?? null;
|
||||||
|
const renderedRows = Array.from(document.querySelectorAll<HTMLElement>(".xterm-rows > div"));
|
||||||
|
const firstRow = renderedRows[0] ?? null;
|
||||||
|
const lastRow = renderedRows.at(-1) ?? null;
|
||||||
|
const surfaceRect = surface?.getBoundingClientRect() ?? null;
|
||||||
|
const firstRowRect = firstRow?.getBoundingClientRect() ?? null;
|
||||||
|
const lastRowRect = lastRow?.getBoundingClientRect() ?? null;
|
||||||
|
const term = (
|
||||||
|
window as Window & {
|
||||||
|
__paseoTerminal?: {
|
||||||
|
rows?: number;
|
||||||
|
cols?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).__paseoTerminal;
|
||||||
|
|
||||||
|
return {
|
||||||
|
visibleSurfaceCount: visibleSurfaces.length,
|
||||||
|
surfaceHeight: surfaceRect?.height ?? 0,
|
||||||
|
rowCount: renderedRows.length,
|
||||||
|
rows: typeof term?.rows === "number" ? term.rows : null,
|
||||||
|
cols: typeof term?.cols === "number" ? term.cols : null,
|
||||||
|
renderedRowsHeight:
|
||||||
|
firstRowRect && lastRowRect ? Math.max(0, lastRowRect.bottom - firstRowRect.top) : 0,
|
||||||
|
tabIds: currentTabIds,
|
||||||
|
};
|
||||||
|
}, tabIds);
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForAlternateScreenExit(page: Page, afterAlt: string, timeout: number) {
|
async function waitForAlternateScreenExit(page: Page, afterAlt: string, timeout: number) {
|
||||||
let lastBufferText = "";
|
let lastBufferText = "";
|
||||||
let lastProbe = await readTerminalRenderProbe(page);
|
let lastProbe = await readTerminalRenderProbe(page);
|
||||||
@@ -53,6 +102,8 @@ async function waitForAlternateScreenExit(page: Page, afterAlt: string, timeout:
|
|||||||
}
|
}
|
||||||
|
|
||||||
test.describe("Terminal alternate-screen transitions", () => {
|
test.describe("Terminal alternate-screen transitions", () => {
|
||||||
|
test.describe.configure({ timeout: 120_000 });
|
||||||
|
|
||||||
let harness: TerminalE2EHarness;
|
let harness: TerminalE2EHarness;
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
@@ -138,4 +189,62 @@ test.describe("Terminal alternate-screen transitions", () => {
|
|||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("opening vim in a new terminal fills the terminal surface", async ({ page }, testInfo) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1280, height: 900 });
|
||||||
|
await installTerminalRenderProbe(page);
|
||||||
|
|
||||||
|
await withTerminalInApp(page, harness, { name: "vim-layout" }, async () => {
|
||||||
|
await harness.setupPrompt(page);
|
||||||
|
const terminalSurface = harness.terminalSurface(page).first();
|
||||||
|
await expect(terminalSurface).toBeVisible({ timeout: 15_000 });
|
||||||
|
await terminalSurface.click();
|
||||||
|
|
||||||
|
const beforeVim = await readTerminalLayoutMetrics(page);
|
||||||
|
|
||||||
|
await startTerminalFrameSampling(page, 2_000);
|
||||||
|
await terminalSurface.pressSequentially("vim -Nu NONE -n\n", { delay: 0 });
|
||||||
|
await page.waitForTimeout(1_500);
|
||||||
|
|
||||||
|
const probe = await readTerminalRenderProbe(page);
|
||||||
|
const samples: TerminalLayoutMetrics[] = [];
|
||||||
|
for (let index = 0; index < 12; index += 1) {
|
||||||
|
samples.push(await readTerminalLayoutMetrics(page));
|
||||||
|
await page.waitForTimeout(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
await testInfo.attach("reused-terminal-layout-metrics", {
|
||||||
|
body: JSON.stringify(
|
||||||
|
{
|
||||||
|
beforeVim,
|
||||||
|
probe: summarizeTerminalRenderProbe(probe),
|
||||||
|
samples,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
contentType: "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstSample = samples[0];
|
||||||
|
const finalSample = samples.at(-1);
|
||||||
|
expect(firstSample, "expected an initial layout sample").toBeTruthy();
|
||||||
|
expect(finalSample, "expected a final layout sample").toBeTruthy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
finalSample?.visibleSurfaceCount ?? 0,
|
||||||
|
"opening vim should leave exactly one visible terminal surface",
|
||||||
|
).toBe(1);
|
||||||
|
expect(
|
||||||
|
finalSample?.renderedRowsHeight ?? 0,
|
||||||
|
"vim should render rows that fill most of the terminal surface",
|
||||||
|
).toBeGreaterThan((finalSample?.surfaceHeight ?? 0) * 0.75);
|
||||||
|
expect(
|
||||||
|
finalSample?.rowCount ?? 0,
|
||||||
|
"vim should leave a substantial number of rendered rows",
|
||||||
|
).toBeGreaterThan(Math.max(10, Math.floor((beforeVim.rowCount || 0) * 0.75)));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -164,7 +164,10 @@ export function TerminalPane({
|
|||||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||||
|
|
||||||
const scopeKey = useMemo(() => terminalScopeKey({ serverId, cwd }), [serverId, cwd]);
|
const scopeKey = useMemo(() => terminalScopeKey({ serverId, cwd }), [serverId, cwd]);
|
||||||
const lastReportedSizeRef = useRef<{ rows: number; cols: number } | null>(null);
|
// Keep the latest measured size for whichever client currently owns the pane,
|
||||||
|
// but only dedupe resizes that this specific client has already pushed.
|
||||||
|
const measuredTerminalSizeRef = useRef<{ rows: number; cols: number } | null>(null);
|
||||||
|
const lastSentTerminalSizeRef = useRef<{ rows: number; cols: number } | null>(null);
|
||||||
const streamControllerRef = useRef<TerminalStreamController | null>(null);
|
const streamControllerRef = useRef<TerminalStreamController | null>(null);
|
||||||
const workspaceTerminalSession = useMemo(
|
const workspaceTerminalSession = useMemo(
|
||||||
() => getWorkspaceTerminalSession({ scopeKey }),
|
() => getWorkspaceTerminalSession({ scopeKey }),
|
||||||
@@ -210,7 +213,7 @@ export function TerminalPane({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPaneFocused && isWorkspaceFocused && isAppVisible && terminalId) {
|
if (isPaneFocused && isWorkspaceFocused && isAppVisible && terminalId) {
|
||||||
lastReportedSizeRef.current = null;
|
lastSentTerminalSizeRef.current = null;
|
||||||
requestTerminalReflow();
|
requestTerminalReflow();
|
||||||
}
|
}
|
||||||
}, [isAppVisible, isPaneFocused, isWorkspaceFocused, requestTerminalReflow, terminalId]);
|
}, [isAppVisible, isPaneFocused, isWorkspaceFocused, requestTerminalReflow, terminalId]);
|
||||||
@@ -296,7 +299,8 @@ export function TerminalPane({
|
|||||||
}, [client, isConnected, isWorkspaceFocused, workspaceTerminalSession.snapshots]);
|
}, [client, isConnected, isWorkspaceFocused, workspaceTerminalSession.snapshots]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
lastReportedSizeRef.current = null;
|
measuredTerminalSizeRef.current = null;
|
||||||
|
lastSentTerminalSizeRef.current = null;
|
||||||
}, [scopeKey]);
|
}, [scopeKey]);
|
||||||
|
|
||||||
const handleStreamControllerStatus = useCallback((status: TerminalStreamControllerStatus) => {
|
const handleStreamControllerStatus = useCallback((status: TerminalStreamControllerStatus) => {
|
||||||
@@ -316,7 +320,7 @@ export function TerminalPane({
|
|||||||
|
|
||||||
const controller = new TerminalStreamController({
|
const controller = new TerminalStreamController({
|
||||||
client,
|
client,
|
||||||
getPreferredSize: () => lastReportedSizeRef.current,
|
getPreferredSize: () => measuredTerminalSizeRef.current,
|
||||||
onOutput: ({ terminalId: outputTerminalId, text }) => {
|
onOutput: ({ terminalId: outputTerminalId, text }) => {
|
||||||
if (!isWorkspaceFocused || terminalIdRef.current !== outputTerminalId) {
|
if (!isWorkspaceFocused || terminalIdRef.current !== outputTerminalId) {
|
||||||
return;
|
return;
|
||||||
@@ -532,24 +536,25 @@ export function TerminalPane({
|
|||||||
|
|
||||||
const handleTerminalResize = useStableEvent((input: { rows: number; cols: number }) => {
|
const handleTerminalResize = useStableEvent((input: { rows: number; cols: number }) => {
|
||||||
const { rows, cols } = input;
|
const { rows, cols } = input;
|
||||||
if (
|
if (rows <= 0 || cols <= 0) {
|
||||||
!client ||
|
|
||||||
!terminalId ||
|
|
||||||
!isPaneFocused ||
|
|
||||||
!isWorkspaceFocused ||
|
|
||||||
!isAppVisible ||
|
|
||||||
rows <= 0 ||
|
|
||||||
cols <= 0
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const normalizedRows = Math.floor(rows);
|
const normalizedRows = Math.floor(rows);
|
||||||
const normalizedCols = Math.floor(cols);
|
const normalizedCols = Math.floor(cols);
|
||||||
const previous = lastReportedSizeRef.current;
|
const nextSize = { rows: normalizedRows, cols: normalizedCols };
|
||||||
if (previous && previous.rows === normalizedRows && previous.cols === normalizedCols) {
|
measuredTerminalSizeRef.current = nextSize;
|
||||||
|
if (!client || !terminalId || !isPaneFocused || !isWorkspaceFocused || !isAppVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastReportedSizeRef.current = { rows: normalizedRows, cols: normalizedCols };
|
const previousSent = lastSentTerminalSizeRef.current;
|
||||||
|
if (
|
||||||
|
previousSent &&
|
||||||
|
previousSent.rows === normalizedRows &&
|
||||||
|
previousSent.cols === normalizedCols
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastSentTerminalSizeRef.current = nextSize;
|
||||||
client.sendTerminalInput(terminalId, {
|
client.sendTerminalInput(terminalId, {
|
||||||
type: "resize",
|
type: "resize",
|
||||||
rows: normalizedRows,
|
rows: normalizedRows,
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ app.setName("Paseo");
|
|||||||
// In dev mode, detect git worktrees and isolate each instance so multiple
|
// In dev mode, detect git worktrees and isolate each instance so multiple
|
||||||
// Electron windows can run side-by-side (separate userData = separate lock).
|
// Electron windows can run side-by-side (separate userData = separate lock).
|
||||||
let devWorktreeName: string | null = null;
|
let devWorktreeName: string | null = null;
|
||||||
if (!app.isPackaged) {
|
const forcedUserDataDir = process.env.PASEO_ELECTRON_USER_DATA_DIR?.trim();
|
||||||
|
if (forcedUserDataDir) {
|
||||||
|
app.setPath("userData", forcedUserDataDir);
|
||||||
|
log.info("[dev-user-data] forced userData dir:", forcedUserDataDir);
|
||||||
|
} else if (!app.isPackaged) {
|
||||||
try {
|
try {
|
||||||
const topLevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
const topLevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
|
|||||||
Reference in New Issue
Block a user