terminal: search.viewport supports dirty tracking for more efficient

pull/9687/head
Mitchell Hashimoto 2025-11-15 20:28:45 -08:00
parent e49f4a6dbc
commit 72921741e8
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
1 changed files with 79 additions and 11 deletions

View File

@ -27,6 +27,12 @@ pub const ViewportSearch = struct {
window: SlidingWindow,
fingerprint: ?Fingerprint,
/// If this is null, then active dirty tracking is disabled and if the
/// viewport overlaps the active area we always re-search. If this is
/// non-null, then we only re-search if the active area is dirty. Dirty
/// marking is up to the caller.
active_dirty: ?bool,
pub fn init(
alloc: Allocator,
needle_unowned: []const u8,
@ -36,7 +42,11 @@ pub const ViewportSearch = struct {
// a small amount of work to reverse things.
var window: SlidingWindow = try .init(alloc, .forward, needle_unowned);
errdefer window.deinit();
return .{ .window = window, .fingerprint = null };
return .{
.window = window,
.fingerprint = null,
.active_dirty = null,
};
}
pub fn deinit(self: *ViewportSearch) void {
@ -75,17 +85,29 @@ pub const ViewportSearch = struct {
var fingerprint: Fingerprint = try .init(self.window.alloc, list);
if (self.fingerprint) |*old| {
if (old.eql(fingerprint)) match: {
// If our fingerprint contains the active area, then we always
// re-search since the active area is mutable.
const active_tl = list.getTopLeft(.active);
const active_br = list.getBottomRight(.active).?;
// Determine if we need to check if we overlap the active
// area. If we have dirty tracking on we also set it to
// false here.
const check_active: bool = active: {
const dirty = self.active_dirty orelse break :active true;
if (!dirty) break :active false;
self.active_dirty = false;
break :active true;
};
// 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;
if (check_active) {
// If our fingerprint contains the active area, then we always
// re-search since the active area is mutable.
const active_tl = list.getTopLeft(.active);
const active_br = list.getBottomRight(.active).?;
// 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;
}
}
// No change
@ -267,6 +289,52 @@ test "clear screen and search" {
try testing.expect(search.next() == null);
}
test "clear screen and search dirty tracking" {
const alloc = testing.allocator;
var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("Fizz\r\nBuzz\r\nFizz\r\nBang");
var search: ViewportSearch = try .init(alloc, "Fizz");
defer search.deinit();
// Turn on dirty tracking
search.active_dirty = false;
// Should update since we've never searched before
try testing.expect(try search.update(&t.screens.active.pages));
// Should not update since nothing changed
try testing.expect(!try search.update(&t.screens.active.pages));
try s.nextSlice("\x1b[2J"); // Clear screen
try s.nextSlice("\x1b[H"); // Move cursor home
try s.nextSlice("Buzz\r\nFizz\r\nBuzz");
// Should still not update since active area isn't dirty
try testing.expect(!try search.update(&t.screens.active.pages));
// Mark
search.active_dirty = true;
try testing.expect(try search.update(&t.screens.active.pages));
{
const sel = search.next().?;
try testing.expectEqual(point.Point{ .active = .{
.x = 0,
.y = 1,
} }, t.screens.active.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 3,
.y = 1,
} }, t.screens.active.pages.pointFromPin(.active, sel.end()).?);
}
try testing.expect(search.next() == null);
}
test "history search, no active area" {
const alloc = testing.allocator;
var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 2 });