terminal/search: drop pruned selections

Partial history erasure can remove a page without marking tracked pins as garbage because they move coherently to the next page. ScreenSearch therefore retained a selection whose cached history result was subsequently removed by pruneHistory. Selecting again indexed an empty history result list and panicked.

Clear the tracked selection when its combined result index falls within the history suffix being pruned. Retained active and newer history selections keep their existing indices.
This commit is contained in:
Mitchell Hashimoto
2026-07-09 22:53:06 -07:00
parent cfce1cd56c
commit d239f98056

View File

@@ -337,6 +337,13 @@ pub const ScreenSearch = struct {
// Everything from here forward we assume is invalid because
// our history results only get older.
const alloc = self.allocator();
if (self.selected) |*m| {
const first_pruned = self.active_results.items.len + i;
if (m.idx >= first_pruned) {
m.deinit(self.screen);
self.selected = null;
}
}
for (self.history_results.items[i..]) |*prune_hl| prune_hl.deinit(alloc);
self.history_results.shrinkAndFree(alloc, i);
return;
@@ -1497,6 +1504,38 @@ test "select after all matches disappear drops the selection" {
try testing.expectEqual(0, search.history_results.items.len);
}
test "select after partial history erase drops a pruned selection" {
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();
stream.nextSlice("error\r\n");
const first = list.pages.first.?;
while (list.totalPages() < 3) stream.nextSlice("\r\n");
const first_rows = first.rows();
var search: ScreenSearch = try .init(alloc, t.screens.active, "error");
defer search.deinit();
try search.searchAll();
try testing.expectEqual(0, search.active_results.items.len);
try testing.expectEqual(1, search.history_results.items.len);
try testing.expect(try search.select(.next));
list.eraseHistory(.{ .history = .{ .y = first_rows - 1 } });
try testing.expect(!search.selected.?.highlight.start.garbage);
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, .{