mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-12 12:19:47 +00:00
terminal: invalidate split source page refs
PageList.split moved the source suffix into a fresh target but left the shortened source on its old generation. Cached matches in that suffix could therefore pass validation against the live source pointer and reach pin tracking with invalid coordinates. Renew the source generation only after target cloning succeeds, and mark compression activity so restored history is reconsidered. The conservative floor keeps bounded pruning safe even when the renewed source and fresh target precede older successors.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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, .{
|
||||
|
||||
Reference in New Issue
Block a user