From 5bfbadbc7044977fcfe6767b79857ead625b6463 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2026 20:22:22 -0800 Subject: [PATCH] terminal/search: screen search prunes history for no-scrollback screens The big comment in `search/screen.zig` describes the solution well. The problem is that our search is discrete by page and a page can contain some amount of history as well. For zero-scrollback screens, we need to fully prune any history lines. For everyone else, everything in the PageList is scrollable and visible so we should search it. --- src/terminal/highlight.zig | 9 ++++ src/terminal/search/screen.zig | 79 +++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/terminal/highlight.zig b/src/terminal/highlight.zig index 582ef6f06..bc3a1758e 100644 --- a/src/terminal/highlight.zig +++ b/src/terminal/highlight.zig @@ -180,6 +180,15 @@ pub const Flattened = struct { }; } + pub fn endPin(self: Flattened) Pin { + const slice = self.chunks.slice(); + return .{ + .node = slice.items(.node)[slice.len - 1], + .x = self.bot_x, + .y = slice.items(.end)[slice.len - 1] - 1, + }; + } + /// Convert to an Untracked highlight. pub fn untracked(self: Flattened) Untracked { // Note: we don't use startPin/endPin here because it is slightly diff --git a/src/terminal/search/screen.zig b/src/terminal/search/screen.zig index 0ae7f8a1f..179e7da87 100644 --- a/src/terminal/search/screen.zig +++ b/src/terminal/search/screen.zig @@ -412,6 +412,12 @@ pub const ScreenSearch = struct { // pages then we need to re-search the pages and add it to // our history results. + // If our screen has no scrollback then we have no history. + if (self.screen.no_scrollback) { + assert(self.history == null); + break :history; + } + const history_: ?*HistorySearch = if (self.history) |*h| state: { // If our start pin became garbage, it means we pruned all // the way up through it, so we have no history anymore. @@ -575,8 +581,43 @@ pub const ScreenSearch = struct { }, } - // Active area search was successful. Now we have to fixup our - // selection if we had one. + // If we have no scrollback, we need to prune any active results + // that aren't in the actual active area. We only do this for the + // no scrollback scenario because with scrollback we actually + // rely on our active search searching by page to find history + // items as well. This is all related to the fact that PageList + // scrollback limits are discrete by page size except we special + // case zero. + if (self.screen.no_scrollback and + self.active_results.items.len > 0) + active_prune: { + const items = self.active_results.items; + const tl = self.screen.pages.getTopLeft(.active); + for (0.., items) |i, *hl| { + if (!tl.before(hl.endPin())) { + // Deinit because its going to be pruned no matter + // what at some point for not being in the active area. + hl.deinit(alloc); + continue; + } + + // In the active area! Since our results are sorted + // that means everything after this is also in the active + // area, so we prune up to this i. + if (i > 0) self.active_results.replaceRangeAssumeCapacity( + 0, + i, + &.{}, + ); + + break :active_prune; + } + + // None are in the active area... + self.active_results.clearRetainingCapacity(); + } + + // Now we have to fixup our selection if we had one. fixup: { const old_idx = old_selection_idx orelse break :fixup; const m = if (self.selected) |*m| m else break :fixup; @@ -1333,3 +1374,37 @@ test "select prev with history" { } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } } + +test "screen search no scrollback has no history" { + const alloc = testing.allocator; + var t: Terminal = try .init(alloc, .{ + .cols = 10, + .rows = 2, + .max_scrollback = 0, + }); + defer t.deinit(alloc); + + // Alt screen has no scrollback + _ = try t.switchScreen(.alternate); + + var s = t.vtStream(); + defer s.deinit(); + + // This will probably stop working at some point and we'll have + // no way to test it using public APIs, but at the time of writing + // this test, CSI 22 J (scroll complete) pushes into scrollback + // with alt screen. + try s.nextSlice("Fizz\r\n"); + try s.nextSlice("\x1b[22J"); + try s.nextSlice("hello."); + + var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz"); + defer search.deinit(); + try search.searchAll(); + try testing.expectEqual(0, search.active_results.items.len); + + // Get all matches + const matches = try search.matches(alloc); + defer alloc.free(matches); + try testing.expectEqual(0, matches.len); +} -- 2.51.2