From 87b11e08929a7e8d5b80dcb8b14f18a25e969883 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 11 Jan 2026 22:54:03 -0800 Subject: [PATCH 1/3] Add failing tests for #10265 --- src/terminal/Terminal.zig | 92 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 8bb167cd1..903e427d4 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -5419,6 +5419,52 @@ test "Terminal: insertLines top/bottom scroll region" { } } +test "Terminal: insertLines across page boundary marks all shifted rows dirty" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 }); + defer t.deinit(alloc); + + const first_page = t.screens.active.pages.pages.first.?; + const first_page_nrows = first_page.data.capacity.rows; + + // Fill up the first page minus 3 rows + for (0..first_page_nrows - 3) |_| try t.linefeed(); + + // Add content that will cross a page boundary + try t.printString("1AAAA"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("2BBBB"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("3CCCC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("4DDDD"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("5EEEE"); + + // Verify we now have a second page + try testing.expect(first_page.next != null); + + t.setCursorPos(1, 1); + t.clearDirty(); + t.insertLines(1); + + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n1AAAA\n2BBBB\n3CCCC\n4DDDD", str); + } +} + test "Terminal: insertLines (legacy test)" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); @@ -8042,6 +8088,52 @@ test "Terminal: deleteLines colors with bg color" { } } +test "Terminal: deleteLines across page boundary marks all shifted rows dirty" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 }); + defer t.deinit(alloc); + + const first_page = t.screens.active.pages.pages.first.?; + const first_page_nrows = first_page.data.capacity.rows; + + // Fill up the first page minus 3 rows + for (0..first_page_nrows - 3) |_| try t.linefeed(); + + // Add content that will cross a page boundary + try t.printString("1AAAA"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("2BBBB"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("3CCCC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("4DDDD"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("5EEEE"); + + // Verify we now have a second page + try testing.expect(first_page.next != null); + + t.setCursorPos(1, 1); + t.clearDirty(); + t.deleteLines(1); + + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("2BBBB\n3CCCC\n4DDDD\n5EEEE", str); + } +} + test "Terminal: deleteLines (legacy)" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 80 }); From 095c82910b2df5302c75934cb5f7b13bdd759efc Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 11 Jan 2026 22:56:03 -0800 Subject: [PATCH 2/3] Terminal: keep cross-boundary rows dirty in {insert,delete}Lines --- src/terminal/Terminal.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 903e427d4..7b384f34e 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1690,6 +1690,10 @@ pub fn insertLines(self: *Terminal, count: usize) void { // Continue the loop to try handling this row again. continue; }; + + // The clone operation may overwrite the dirty flag, so make + // sure the row is still marked dirty. + dst_row.dirty = true; } else { if (!left_right) { // Swap the src/dst cells. This ensures that our dst gets the @@ -1888,6 +1892,10 @@ pub fn deleteLines(self: *Terminal, count: usize) void { // Continue the loop to try handling this row again. continue; }; + + // The clone operation may overwrite the dirty flag, so make + // sure the row is still marked dirty. + dst_row.dirty = true; } else { if (!left_right) { // Swap the src/dst cells. This ensures that our dst gets the From 257aafb7b44db09252e1ae5a8d63a9e8920f788d Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 12 Jan 2026 09:32:33 -0800 Subject: [PATCH 3/3] Consolidate dirty marking in insertLines/deleteLines --- src/terminal/Terminal.zig | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 7b384f34e..d717a9724 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1602,9 +1602,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { const cur_rac = cur_p.rowAndCell(); const cur_row: *Row = cur_rac.row; - // Mark the row as dirty - cur_p.markDirty(); - // If this is one of the lines we need to shift, do so if (y > adjusted_count) { const off_p = cur_p.up(adjusted_count).?; @@ -1690,10 +1687,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { // Continue the loop to try handling this row again. continue; }; - - // The clone operation may overwrite the dirty flag, so make - // sure the row is still marked dirty. - dst_row.dirty = true; } else { if (!left_right) { // Swap the src/dst cells. This ensures that our dst gets the @@ -1703,9 +1696,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { dst_row.* = src_row.*; src_row.* = dst; - // Make sure the row is marked as dirty though. - dst_row.dirty = true; - // Ensure what we did didn't corrupt the page cur_p.node.data.assertIntegrity(); } else { @@ -1732,6 +1722,9 @@ pub fn insertLines(self: *Terminal, count: usize) void { ); } + // Mark the row as dirty + cur_p.markDirty(); + // We have successfully processed a line. y -= 1; // Move our pin up to the next row. @@ -1809,9 +1802,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void { const cur_rac = cur_p.rowAndCell(); const cur_row: *Row = cur_rac.row; - // Mark the row as dirty - cur_p.markDirty(); - // If this is one of the lines we need to shift, do so if (y < rem - adjusted_count) { const off_p = cur_p.down(adjusted_count).?; @@ -1892,10 +1882,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void { // Continue the loop to try handling this row again. continue; }; - - // The clone operation may overwrite the dirty flag, so make - // sure the row is still marked dirty. - dst_row.dirty = true; } else { if (!left_right) { // Swap the src/dst cells. This ensures that our dst gets the @@ -1905,9 +1891,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void { dst_row.* = src_row.*; src_row.* = dst; - // Make sure the row is marked as dirty though. - dst_row.dirty = true; - // Ensure what we did didn't corrupt the page cur_p.node.data.assertIntegrity(); } else { @@ -1934,6 +1917,9 @@ pub fn deleteLines(self: *Terminal, count: usize) void { ); } + // Mark the row as dirty + cur_p.markDirty(); + // We have successfully processed a line. y += 1; // Move our pin down to the next row.