diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index ea7f00d18..9b0f5838e 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -3252,6 +3252,12 @@ pub fn split( // error handling. It is possible but we haven't written it. errdefer comptime unreachable; + // The source is about to describe a shorter row range. Renew its + // generation only after every failable step has succeeded so a failed + // split leaves existing references valid. + self.invalidateNodeLayout(original_node); + self.page_compression.markActivity(); + // Move any tracked pins from the copied rows for (self.tracked_pins.keys()) |tracked| { if (tracked.node.page() != page or @@ -7053,6 +7059,35 @@ test "PageList partial erase restarts compression before continuation" { try testing.expect(first.isCompressed()); } +test "PageList bounded pruning after split invalidation preserves live serials" { + const testing = std.testing; + + var s = try init( + testing.allocator, + 80, + 24, + 2 * PagePool.item_size, + ); + defer s.deinit(); + + while (s.totalPages() < 2) _ = try s.grow(); + const first = s.pages.first.?; + const old_serial = first.serial; + const activity = s.page_compression.activity_serial; + + try s.split(.{ + .node = first, + .y = first.rows() / 2, + .x = 0, + }); + try testing.expect(!s.nodeIsValid(first, old_serial)); + try testing.expect(activity != s.page_compression.activity_serial); + + try s.fillLastPageForTest(); + _ = try s.grow(); + try s.expectLivePageSerialsValidForTest(); +} + test "PageList repeated bounded pruning after split preserves live serials" { const testing = std.testing; diff --git a/src/terminal/search/screen.zig b/src/terminal/search/screen.zig index 71c6fe184..5bd977240 100644 --- a/src/terminal/search/screen.zig +++ b/src/terminal/search/screen.zig @@ -1649,6 +1649,39 @@ test "select after partial history page erase ignores shifted results" { try testing.expect(search.selected == null); } +test "select after history page split ignores moved results" { + const alloc = testing.allocator; + var t: Terminal = try .init(alloc, .{ + .cols = 10, + .rows = 2, + .max_scrollback = std.math.maxInt(usize), + }); + defer t.deinit(alloc); + const list: *PageList = &t.screens.active.pages; + + var stream = t.vtStream(); + defer stream.deinit(); + + const first = list.pages.first.?; + while (first.rows() < first.capacity().rows) stream.nextSlice("\r\n"); + stream.nextSlice("error"); + for (0..list.rows + 1) |_| stream.nextSlice("\r\n"); + + var search: ScreenSearch = try .init(alloc, t.screens.active, "error"); + defer search.deinit(); + try search.searchAll(); + try testing.expectEqual(1, search.history_results.items.len); + + try list.split(.{ + .node = first, + .y = first.rows() / 2, + .x = 0, + }); + + try testing.expect(!try search.select(.next)); + try testing.expect(search.selected == null); +} + test "screen search no scrollback has no history" { const alloc = testing.allocator; var t: Terminal = try .init(alloc, .{