diff --git a/src/terminal/search/screen.zig b/src/terminal/search/screen.zig index 859759739..e0cd99110 100644 --- a/src/terminal/search/screen.zig +++ b/src/terminal/search/screen.zig @@ -344,6 +344,39 @@ pub const ScreenSearch = struct { } } + fn historyResultIsValid( + self: *const ScreenSearch, + hl: *const FlattenedHighlight, + ) bool { + const chunks = hl.chunks.slice(); + const nodes = chunks.items(.node); + const serials = chunks.items(.serial); + for (nodes, serials) |node, serial| { + if (!self.screen.pages.nodeIsValid(node, serial)) return false; + } + + return true; + } + + /// Clear a selected history match if its flattened page coordinates are + /// stale. reloadActive only inspects the selected history result, so this + /// avoids validating every cached result on each active-area refresh. + fn validateSelectedHistory(self: *ScreenSearch) void { + const m = if (self.selected) |*m| m else return; + const active_len = self.active_results.items.len; + if (m.idx < active_len) return; + + const history_idx = m.idx - active_len; + if (history_idx < self.history_results.items.len and + self.historyResultIsValid(&self.history_results.items[history_idx])) + { + return; + } + + m.deinit(self.screen); + self.selected = null; + } + fn pruneHistory(self: *ScreenSearch) void { // Go through our history results in order (newest to oldest) to find // any result that contains an invalid serial. @@ -374,10 +407,7 @@ pub const ScreenSearch = struct { // Ordinary pruning, layout changes, and replacements do not // advance the epoch. Validate their pointer-plus-generation pairs // against the live list. - const nodes = chunks.items(.node); - for (nodes, serials) |node, serial| { - if (!self.screen.pages.nodeIsValid(node, serial)) break; - } else { + if (self.historyResultIsValid(hl)) { i += 1; continue; } @@ -459,9 +489,9 @@ pub const ScreenSearch = struct { // searchers: column reflow may have freed every node they reference. if (try self.resetIfDimensionsChanged()) return; - // Validate flattened history before any coordinate is converted back - // into a pin or compared with a tracked selection below. - self.pruneHistory(); + // reloadActive only inspects the selected history result, so validate + // that result without scanning every cached match on this hot path. + self.validateSelectedHistory(); const tw = reloadActive_tw; @@ -772,9 +802,11 @@ pub const ScreenSearch = struct { // selected result so no pre-resize pointer is ever dereferenced. _ = try self.resetIfDimensionsChanged(); - // All selection requires valid pins, so reload immediately. Reload - // validates flattened history before inspecting cached coordinates. + // Reload validates the selected history result before inspecting its + // cached coordinates. Prune the remaining results afterward so every + // candidate is valid before selection tracks it. try self.reloadActive(); + self.pruneHistory(); return switch (to) { .next => try self.selectNext(), @@ -1653,6 +1685,40 @@ test "select after partial history page erase ignores shifted results" { try testing.expect(search.selected == null); } +test "reload defers pruning unselected history 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); + + list.eraseHistory(.{ .history = .{ .y = 0 } }); + + // Routine active refreshes don't inspect unselected history results. + try search.reloadActive(); + try testing.expectEqual(1, search.history_results.items.len); + + // Selection prunes every candidate before attempting to track it. + try testing.expect(!try search.select(.next)); + try testing.expectEqual(0, search.history_results.items.len); +} + test "reload after partial history page erase drops shifted selection first" { const alloc = testing.allocator; var t: Terminal = try .init(alloc, .{