diff --git a/src/terminal/search/viewport.zig b/src/terminal/search/viewport.zig index d8dbb9429..0493c0d6f 100644 --- a/src/terminal/search/viewport.zig +++ b/src/terminal/search/viewport.zig @@ -103,9 +103,9 @@ pub const ViewportSearch = struct { // If our viewport contains the start or end of the active area, // we are in the active area. We purposely do this first // because our viewport is always larger than the active area. - for (old.nodes) |node| { - if (node == active_tl.node) break :match; - if (node == active_br.node) break :match; + for (old.entries) |entry| { + if (entry.node == active_tl.node) break :match; + if (entry.node == active_br.node) break :match; } } @@ -135,7 +135,7 @@ pub const ViewportSearch = struct { // exists) so we can cover the overlap. We only do this for the // soft-wrapped prior pages. const overlap_len = self.window.needle.len -| 1; - var node_ = fingerprint.nodes[0].prev; + var node_ = fingerprint.entries[0].node.prev; var added: usize = 0; while (node_) |node| : (node_ = node.prev) { // We could be more accurate here and count bytes since the @@ -149,13 +149,13 @@ pub const ViewportSearch = struct { // We can use our fingerprint nodes to initialize our sliding // window, since we already traversed the viewport once. var end_append: SlidingWindow.AppendResult = undefined; - for (fingerprint.nodes) |node| { - end_append = try self.window.append(node); + for (fingerprint.entries) |entry| { + end_append = try self.window.append(entry.node); } // Add any trailing overlap as well. trailing: { - const end: *PageList.List.Node = fingerprint.nodes[fingerprint.nodes.len - 1]; + const end: *PageList.List.Node = fingerprint.entries[fingerprint.entries.len - 1].node; if (!end_append.last_row_wrapped) break :trailing; node_ = end.next; @@ -181,14 +181,18 @@ pub const ViewportSearch = struct { /// Viewport fingerprint so we can detect when the viewport moves. const Fingerprint = struct { - /// The nodes that make up the viewport. We need to flatten this - /// to a single list because we can't safely traverse the cached values - /// because the page nodes may be invalid. All that is safe is comparing - /// the actual pointer values. - nodes: []const *PageList.List.Node, + /// The node addresses and generations that make up the viewport. We + /// can't safely dereference cached node pointers because the nodes may + /// be invalid, so equality only compares these captured values. + entries: []const Entry, + + const Entry = struct { + node: *PageList.List.Node, + serial: u64, + }; pub fn init(alloc: Allocator, pages: *PageList) Allocator.Error!Fingerprint { - var list: std.ArrayList(*PageList.List.Node) = .empty; + var list: std.ArrayList(Entry) = .empty; defer list.deinit(alloc); // Get our viewport area. Bottom right of a viewport can never @@ -197,18 +201,22 @@ pub const ViewportSearch = struct { const br = pages.getBottomRight(.viewport).?; var it = tl.pageIterator(.right_down, br); - while (it.next()) |chunk| try list.append(alloc, chunk.node); - return .{ .nodes = try list.toOwnedSlice(alloc) }; + while (it.next()) |chunk| try list.append(alloc, .{ + .node = chunk.node, + .serial = chunk.node.serial, + }); + return .{ .entries = try list.toOwnedSlice(alloc) }; } pub fn deinit(self: *Fingerprint, alloc: Allocator) void { - alloc.free(self.nodes); + alloc.free(self.entries); } pub fn eql(self: Fingerprint, other: Fingerprint) bool { - if (self.nodes.len != other.nodes.len) return false; - for (self.nodes, other.nodes) |a, b| { - if (a != b) return false; + if (self.entries.len != other.entries.len) return false; + for (self.entries, other.entries) |a, b| { + if (a.node != b.node) return false; + if (a.serial != b.serial) return false; } return true; } @@ -258,6 +266,33 @@ test "simple search" { try testing.expect(search.next() == null); } +test "fingerprint detects node generation changes" { + const alloc = testing.allocator; + var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 10 }); + defer t.deinit(alloc); + + var search: ViewportSearch = try .init(alloc, "needle"); + defer search.deinit(); + search.active_dirty = false; + + try testing.expect(try search.update(&t.screens.active.pages)); + try testing.expect(!try search.update(&t.screens.active.pages)); + + const old_entry = search.fingerprint.?.entries[0]; + const node = t.screens.active.pages.pages.first.?; + try testing.expectEqual(node, old_entry.node); + const original_serial = node.serial; + defer node.serial = original_serial; + node.serial += 1; + + // The viewport still contains the same pointer, but its generation has + // changed, so the cached window must be rebuilt. + try testing.expect(try search.update(&t.screens.active.pages)); + const new_entry = search.fingerprint.?.entries[0]; + try testing.expectEqual(old_entry.node, new_entry.node); + try testing.expect(old_entry.serial != new_entry.serial); +} + test "clear screen and search" { const alloc = testing.allocator; var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 10 });