mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-12 12:19:47 +00:00
terminal/search: avoid pruning history on reload
reloadActive previously pruned every cached history result whenever the active area changed. Search reconciliation runs under the terminal lock, so this scaled with both result and page counts on a frame-paced path. Validate only the selected history result during reload. Keep full pruning before selection navigation so stale candidates are removed before their coordinates are tracked.
This commit is contained in:
@@ -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, .{
|
||||
|
||||
Reference in New Issue
Block a user