fix(tui): auto-clear empty rows when content shrinks (#1095)

- Add height change detection (analogous to existing width detection)
- Auto-detect when content shrinks below maxLinesRendered and trigger
  full re-render to clear leftover empty rows
- Only triggers when no overlays are active (overlays need padding)

Fixes empty rows appearing below footer when:
- Closing /tree or other selectors
- Clearing multi-line editor content
- Any component shrinking

Also adds regression tests for resize handling and content shrinkage.
This commit is contained in:
Marc Krenn 2026-01-30 20:45:34 +01:00 committed by GitHub
parent bb0c2bf77a
commit 39c898d7c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 144 additions and 4 deletions

View file

@ -22,6 +22,133 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num
return cell.isItalic();
}
describe("TUI resize handling", () => {
it("triggers full re-render when terminal height changes", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["Line 0", "Line 1", "Line 2"];
tui.start();
await terminal.flush();
const initialRedraws = tui.fullRedraws;
// Resize height
terminal.resize(40, 15);
await terminal.flush();
// Should have triggered a full redraw
assert.ok(tui.fullRedraws > initialRedraws, "Height change should trigger full redraw");
const viewport = terminal.getViewport();
assert.ok(viewport[0]?.includes("Line 0"), "Content preserved after height change");
tui.stop();
});
it("triggers full re-render when terminal width changes", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["Line 0", "Line 1", "Line 2"];
tui.start();
await terminal.flush();
const initialRedraws = tui.fullRedraws;
// Resize width
terminal.resize(60, 10);
await terminal.flush();
// Should have triggered a full redraw
assert.ok(tui.fullRedraws > initialRedraws, "Width change should trigger full redraw");
tui.stop();
});
});
describe("TUI content shrinkage", () => {
it("clears empty rows when content shrinks significantly", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
// Start with many lines
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3", "Line 4", "Line 5"];
tui.start();
await terminal.flush();
const initialRedraws = tui.fullRedraws;
// Shrink to fewer lines
component.lines = ["Line 0", "Line 1"];
tui.requestRender();
await terminal.flush();
// Should have triggered a full redraw to clear empty rows
assert.ok(tui.fullRedraws > initialRedraws, "Content shrinkage should trigger full redraw");
const viewport = terminal.getViewport();
assert.ok(viewport[0]?.includes("Line 0"), "First line preserved");
assert.ok(viewport[1]?.includes("Line 1"), "Second line preserved");
// Lines below should be empty (cleared)
assert.strictEqual(viewport[2]?.trim(), "", "Line 2 should be cleared");
assert.strictEqual(viewport[3]?.trim(), "", "Line 3 should be cleared");
tui.stop();
});
it("handles shrink to single line", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3"];
tui.start();
await terminal.flush();
// Shrink to single line
component.lines = ["Only line"];
tui.requestRender();
await terminal.flush();
const viewport = terminal.getViewport();
assert.ok(viewport[0]?.includes("Only line"), "Single line rendered");
assert.strictEqual(viewport[1]?.trim(), "", "Line 1 should be cleared");
tui.stop();
});
it("handles shrink to empty", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["Line 0", "Line 1", "Line 2"];
tui.start();
await terminal.flush();
// Shrink to empty
component.lines = [];
tui.requestRender();
await terminal.flush();
const viewport = terminal.getViewport();
// All lines should be empty
assert.strictEqual(viewport[0]?.trim(), "", "Line 0 should be cleared");
assert.strictEqual(viewport[1]?.trim(), "", "Line 1 should be cleared");
tui.stop();
});
});
describe("TUI differential rendering", () => {
it("tracks cursor correctly when content shrinks with unchanged remaining lines", async () => {
const terminal = new VirtualTerminal(40, 10);