terminal: search.Thread searches viewport and notifies viewport results

pull/9602/head
Mitchell Hashimoto 2025-11-14 21:36:43 -08:00
parent 99d47a4627
commit acab8c90a2
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
2 changed files with 138 additions and 37 deletions

View File

@ -16,11 +16,15 @@ const Mutex = std.Thread.Mutex;
const xev = @import("../../global.zig").xev;
const internal_os = @import("../../os/main.zig");
const BlockingQueue = @import("../../datastruct/main.zig").BlockingQueue;
const point = @import("../point.zig");
const PageList = @import("../PageList.zig");
const Screen = @import("../Screen.zig");
const ScreenSet = @import("../ScreenSet.zig");
const Selection = @import("../Selection.zig");
const Terminal = @import("../Terminal.zig");
const ScreenSearch = @import("screen.zig").ScreenSearch;
const ViewportSearch = @import("viewport.zig").ViewportSearch;
const log = std.log.scoped(.search_thread);
@ -150,6 +154,17 @@ fn threadMain_(self: *Thread) !void {
continue;
};
// If we have an active search, we always send any pending
// notifications. Even if the search is complete, there may be
// notifications to send.
if (self.opts.event_cb) |cb| {
s.notify(
self.alloc,
cb,
self.opts.event_userdata,
);
}
if (s.isComplete()) {
// If our search is complete, there's no more work to do, we
// can block until we have an xev action.
@ -170,31 +185,25 @@ fn threadMain_(self: *Thread) !void {
.blocked => {
self.opts.mutex.lock();
defer self.opts.mutex.unlock();
try s.feed(
self.alloc,
self.opts.terminal,
);
s.feed(self.alloc, self.opts.terminal);
},
}
// Publish any notifications about search state changes.
// Ticking can complete our search.
if (s.isComplete()) {
if (self.opts.event_cb) |cb| {
s.notify(
cb,
self.opts.event_userdata,
);
// If our forward progress resulted in us becoming complete,
// then notify our callback.
if (s.isComplete()) cb(
cb(
.complete,
self.opts.event_userdata,
);
}
}
// We have an active search, so we only want to process messages
// we have but otherwise return immediately so we can continue the
// search.
// search. If the above completed the search, we still want to
// go around the loop as quickly as possible to send notifications,
// and then we'll block on the loop next time.
try self.loop.run(.no_wait);
}
}
@ -222,11 +231,13 @@ fn changeNeedle(self: *Thread, needle: []const u8) !void {
// No needle means stop the search.
if (needle.len == 0) return;
// We need to grab the terminal lock to setup our search state.
// Setup our search state.
self.search = try .init(self.alloc, needle);
// We need to grab the terminal lock and do an initial feed.
self.opts.mutex.lock();
defer self.opts.mutex.unlock();
const t: *Terminal = self.opts.terminal;
self.search = try .init(self.alloc, needle, t);
self.search.?.feed(self.alloc, self.opts.terminal);
}
fn wakeupCallback(
@ -297,10 +308,17 @@ pub const Event = union(enum) {
/// Total matches on the current active screen have changed.
total_matches: usize,
/// Matches in the viewport have changed. The memory is owned by the
/// search thread and is only valid during the callback.
viewport_matches: []const Selection,
};
/// Search state.
const Search = struct {
/// Active viewport search for the active screen.
viewport: ViewportSearch,
/// The searchers for all the screens.
screens: std.EnumMap(ScreenSet.Key, ScreenSearch),
@ -310,29 +328,27 @@ const Search = struct {
/// The last total matches reported.
last_total: ?usize,
/// The last viewport matches we found.
stale_viewport_matches: bool,
pub fn init(
alloc: Allocator,
needle: []const u8,
t: *Terminal,
) Allocator.Error!Search {
// We only initialize the primary screen for now. Our reconciler
// via feed will handle setting up our other screens. We just need
// to setup at least one here so that we can store our needle.
var screen_search: ScreenSearch = try .init(
alloc,
t.screens.get(.primary).?,
needle,
);
errdefer screen_search.deinit();
var vp: ViewportSearch = try .init(alloc, needle);
errdefer vp.deinit();
return .{
.screens = .init(.{ .primary = screen_search }),
.viewport = vp,
.screens = .init(.{}),
.last_active_screen = .primary,
.last_total = null,
.stale_viewport_matches = true,
};
}
pub fn deinit(self: *Search) void {
self.viewport.deinit();
var it = self.screens.iterator();
while (it.next()) |entry| entry.value.deinit();
}
@ -402,7 +418,7 @@ const Search = struct {
self: *Search,
alloc: Allocator,
t: *Terminal,
) !void {
) void {
// Update our active screen
if (t.screens.active_key != self.last_active_screen) {
self.last_active_screen = t.screens.active_key;
@ -439,7 +455,7 @@ const Search = struct {
self.screens.put(entry.key, ScreenSearch.init(
alloc,
entry.value.*,
self.screens.get(.primary).?.needle(),
self.viewport.needle(),
) catch |err| switch (err) {
error.OutOfMemory => {
// OOM is probably going to sink the entire ship but
@ -455,11 +471,26 @@ const Search = struct {
}
}
// Check our viewport for changes.
if (self.viewport.update(&t.screens.active.pages)) |updated| {
if (updated) self.stale_viewport_matches = true;
} else |err| switch (err) {
error.OutOfMemory => log.warn(
"error updating viewport search err={}",
.{err},
),
}
// Feed data
var it = self.screens.iterator();
while (it.next()) |entry| {
if (entry.value.state.needsFeed()) {
try entry.value.feed();
entry.value.feed() catch |err| switch (err) {
error.OutOfMemory => log.warn(
"error feeding screen search key={} err={}",
.{ entry.key, err },
),
};
}
}
}
@ -469,15 +500,49 @@ const Search = struct {
/// This doesn't require any locking as it only reads internal state.
pub fn notify(
self: *Search,
alloc: Allocator,
cb: EventCallback,
ud: ?*anyopaque,
) void {
const screen_search = self.screens.get(self.last_active_screen) orelse return;
// Check our total match data
const total = screen_search.matchesLen();
if (total != self.last_total) {
self.last_total = total;
cb(.{ .total_matches = total }, ud);
}
// Check our viewport matches. If they're stale, we do the
// viewport search now. We do this as part of notify and not
// tick because the viewport search is very fast and doesn't
// require ticked progress or feeds.
if (self.stale_viewport_matches) viewport: {
// We always make stale as false. Even if we fail below
// we require a re-feed to re-search the viewport. The feed
// process will make it stale again.
self.stale_viewport_matches = false;
var results: std.ArrayList(Selection) = .empty;
defer results.deinit(alloc);
while (self.viewport.next()) |sel| {
results.append(alloc, sel) catch |err| switch (err) {
error.OutOfMemory => {
log.warn(
"error collecting viewport matches err={}",
.{err},
);
// Reset the viewport so we force an update on the
// next feed.
self.viewport.reset();
break :viewport;
},
};
}
cb(.{ .viewport_matches = results.items }, ud);
}
}
};
@ -486,12 +551,20 @@ test {
const Self = @This();
reset: std.Thread.ResetEvent = .{},
total: usize = 0,
viewport: []const Selection = &.{},
fn callback(event: Event, userdata: ?*anyopaque) void {
const ud: *Self = @ptrCast(@alignCast(userdata.?));
switch (event) {
.complete => ud.reset.set(),
.total_matches => |v| ud.total = v,
.viewport_matches => |v| {
testing.allocator.free(ud.viewport);
ud.viewport = testing.allocator.dupe(
Selection,
v,
) catch unreachable;
},
}
}
};
@ -506,6 +579,7 @@ test {
try stream.nextSlice("Hello, world");
var ud: UserData = .{};
defer alloc.free(ud.viewport);
var thread: Thread = try .init(alloc, .{
.mutex = &mutex,
.terminal = &t,
@ -534,5 +608,18 @@ test {
try thread.stop.notify();
os_thread.join();
// 1 total matches
try testing.expectEqual(1, ud.total);
try testing.expectEqual(1, ud.viewport.len);
{
const sel = ud.viewport[0];
try testing.expectEqual(point.Point{ .screen = .{
.x = 7,
.y = 0,
} }, t.screens.active.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 11,
.y = 0,
} }, t.screens.active.pages.pointFromPin(.screen, sel.end()).?);
}
}

View File

@ -28,12 +28,12 @@ pub const ViewportSearch = struct {
pub fn init(
alloc: Allocator,
needle: []const u8,
needle_unowned: []const u8,
) Allocator.Error!ViewportSearch {
// We just do a forward search since the viewport is usually
// pretty small so search results are instant anyways. This avoids
// a small amount of work to reverse things.
var window: SlidingWindow = try .init(alloc, .forward, needle);
var window: SlidingWindow = try .init(alloc, .forward, needle_unowned);
errdefer window.deinit();
return .{ .window = window, .fingerprint = null };
}
@ -43,6 +43,20 @@ pub const ViewportSearch = struct {
self.window.deinit();
}
/// Reset our fingerprint and results so that the next update will
/// always re-search.
pub fn reset(self: *ViewportSearch) void {
if (self.fingerprint) |*fp| fp.deinit(self.window.alloc);
self.fingerprint = null;
self.window.clearAndRetainCapacity();
}
/// The needle that this search is using.
pub fn needle(self: *const ViewportSearch) []const u8 {
assert(self.window.direction == .forward);
return self.window.needle;
}
/// Update the sliding window to reflect the current viewport. This
/// will do nothing if the viewport hasn't changed since the last
/// search.