diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 5df2c2b91..2888010b2 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1047,6 +1047,16 @@ fn resizeCols( const wrapped = wrapped: { var wrapped: usize = 0; + // If shrinking rows (in the .lt branch of resize, rows shrink + // before we get here) pushed the cursor pin above the new active + // area, there are no rows to count and iterating .left_up toward + // the active-area top would be an invalid (reversed) range. The + // preserved-cursor growth below already no-ops for a cursor that + // isn't in the active area, so we just count zero here. + if (active_pin) |ap| { + if (p.before(ap)) break :wrapped 0; + } + var row_it = p.rowIterator(.left_up, active_pin); while (row_it.next()) |next| { const row = next.rowAndCell().row; @@ -10859,6 +10869,58 @@ test "PageList resize less rows and cols cursor at bottom" { } }, s.pointFromPin(.active, cursor_pin.*).?); } +test "PageList resize less rows and cols cursor near top pushed to scrollback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Fill every active row with non-blank content so that shrinking rows + // can't trim trailing blank lines and instead pushes the top rows into + // scrollback. + { + var it = s.rowIterator(.right_down, .{ .active = .{} }, null); + while (it.next()) |p| { + const rac = p.rowAndCell(); + const cells = p.node.data.getCells(rac.row); + for (cells, 0..) |*cell, x| cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast('A' + (x % 26)) }, + }; + } + } + + // Cursor near the top of the active area. After we shrink rows the active + // area top moves down past this pin, so it ends up in scrollback. + const cursor_pin = try s.trackPin(s.pin(.{ .active = .{ + .x = 0, + .y = 0, + } }).?); + defer s.untrackPin(cursor_pin); + + // Shrink both axes with reflow. resizeWithoutReflow shrinks self.rows + // first, leaving the cursor pin above the new active area, then resizeCols + // walks .left_up from the cursor pin toward the active-area top. + try s.resize(.{ + .cols = 79, + .rows = 20, + .reflow = true, + .cursor = .{ .x = 0, .y = 0, .pin = cursor_pin }, + }); + try testing.expectEqual(@as(usize, 79), s.cols); + try testing.expectEqual(@as(usize, 20), s.rows); + + // The active area is anchored to the bottom, so shrinking rows pushed the + // top-of-screen cursor into scrollback: it no longer resolves to an + // active-area coordinate, but it remains a valid screen pin. + try testing.expect(s.pointFromPin(.active, cursor_pin.*) == null); + try testing.expect(s.pointFromPin(.screen, cursor_pin.*) != null); + + // Integrity must hold after the resize. + s.assertIntegrity(); +} + test "PageList resize (no reflow) more rows and less cols" { const testing = std.testing; const alloc = testing.allocator;