terminal: search thread active screen reconciliation loop

pull/9602/head
Mitchell Hashimoto 2025-11-14 21:24:17 -08:00
parent 1867928b84
commit bfa397b196
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
2 changed files with 97 additions and 61 deletions

View File

@ -161,25 +161,19 @@ fn threadMain_(self: *Thread) !void {
// for data loading, etc.
switch (s.tick()) {
// We're complete now when we were not before. Notify!
.complete => if (self.opts.event_cb) |cb| {
cb(.complete, self.opts.event_userdata);
},
.complete => {},
// Forward progress was made.
.progress => {},
// All searches are blocked. Let's grab the lock and feed data.
.blocked => {
try s.feed(self.opts.mutex, self.opts.terminal);
// Feeding can result in completion if there is no more
// data to feed. If we transitioned to complete, notify!
if (self.opts.event_cb) |cb| {
if (s.isComplete()) cb(
.complete,
self.opts.event_userdata,
);
}
self.opts.mutex.lock();
defer self.opts.mutex.unlock();
try s.feed(
self.alloc,
self.opts.terminal,
);
},
}
@ -189,6 +183,13 @@ fn threadMain_(self: *Thread) !void {
cb,
self.opts.event_userdata,
);
// If our forward progress resulted in us becoming complete,
// then notify our callback.
if (s.isComplete()) cb(
.complete,
self.opts.event_userdata,
);
}
// We have an active search, so we only want to process messages
@ -221,42 +222,11 @@ fn changeNeedle(self: *Thread, needle: []const u8) !void {
// No needle means stop the search.
if (needle.len == 0) return;
// Our new search state
var search: Search = .empty;
errdefer search.deinit();
// We need to grab the terminal lock to setup our search state.
self.opts.mutex.lock();
defer self.opts.mutex.unlock();
const t: *Terminal = self.opts.terminal;
// Go through all our screens, setup our search state.
//
// NOTE(mitchellh): Maybe we should only initialize the screen we're
// currently looking at (the active screen) and then let our screen
// reconciliation timer add the others later in order to minimize
// startup latency.
var it = t.screens.all.iterator();
while (it.next()) |entry| {
var screen_search: ScreenSearch = ScreenSearch.init(
self.alloc,
entry.value.*,
needle,
) catch |err| switch (err) {
error.OutOfMemory => {
// We can ignore this (although OOM probably means the whole
// ship is sinking). Our reconciliation timer will try again
// later.
log.warn("error initializing screen search key={} err={}", .{ entry.key, err });
continue;
},
};
errdefer screen_search.deinit();
search.screens.put(entry.key, screen_search);
}
// Our search state is setup
self.search = search;
self.search = try .init(self.alloc, needle, t);
}
fn wakeupCallback(
@ -340,11 +310,27 @@ const Search = struct {
/// The last total matches reported.
last_total: ?usize,
pub const empty: Search = .{
.screens = .init(.{}),
.last_active_screen = .primary,
.last_total = null,
};
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();
return .{
.screens = .init(.{ .primary = screen_search }),
.last_active_screen = .primary,
.last_total = null,
};
}
pub fn deinit(self: *Search) void {
var it = self.screens.iterator();
@ -412,16 +398,63 @@ const Search = struct {
/// Grab the mutex and update any state that requires it, such as
/// feeding additional data to the searches or updating the active screen.
pub fn feed(self: *Search, mutex: *Mutex, t: *Terminal) !void {
mutex.lock();
defer mutex.unlock();
pub fn feed(
self: *Search,
alloc: Allocator,
t: *Terminal,
) !void {
// Update our active screen
if (t.screens.active_key != self.last_active_screen) {
self.last_active_screen = t.screens.active_key;
self.last_total = null; // force notification
}
// Reconcile our screens with the terminal screens. Remove
// searchers for screens that no longer exist and add searchers
// for screens that do exist but we don't have yet.
{
// Remove screens we have that no longer exist or changed.
var it = self.screens.iterator();
while (it.next()) |entry| {
const remove: bool = remove: {
// If the screen doesn't exist at all, remove it.
const actual = t.screens.all.get(entry.key) orelse break :remove true;
// If the screen pointer changed, remove it, the screen
// was totally reinitialized.
break :remove actual != entry.value.screen;
};
if (remove) {
entry.value.deinit();
_ = self.screens.remove(entry.key);
}
}
}
{
// Add screens that exist but we don't have yet.
var it = t.screens.all.iterator();
while (it.next()) |entry| {
if (self.screens.contains(entry.key)) continue;
self.screens.put(entry.key, ScreenSearch.init(
alloc,
entry.value.*,
self.screens.get(.primary).?.needle(),
) catch |err| switch (err) {
error.OutOfMemory => {
// OOM is probably going to sink the entire ship but
// we can just ignore it and wait on the next
// reconciliation to try again.
log.warn(
"error initializing screen search for key={} err={}",
.{ entry.key, err },
);
continue;
},
});
}
}
// Feed data
var it = self.screens.iterator();
while (it.next()) |entry| {

View File

@ -98,11 +98,11 @@ pub const ScreenSearch = struct {
pub fn init(
alloc: Allocator,
screen: *Screen,
needle: []const u8,
needle_unowned: []const u8,
) Allocator.Error!ScreenSearch {
var result: ScreenSearch = .{
.screen = screen,
.active = try .init(alloc, needle),
.active = try .init(alloc, needle_unowned),
.history = null,
.state = .active,
.active_results = .empty,
@ -128,6 +128,12 @@ pub const ScreenSearch = struct {
return self.active.window.alloc;
}
/// The needle that this search is using.
pub fn needle(self: *const ScreenSearch) []const u8 {
assert(self.active.window.direction == .forward);
return self.active.window.needle;
}
/// Returns the total number of matches found so far.
pub fn matchesLen(self: *const ScreenSearch) usize {
return self.active_results.items.len + self.history_results.items.len;
@ -310,12 +316,9 @@ pub const ScreenSearch = struct {
// No history search yet, but we now have history. So let's
// initialize.
// Our usage of needle below depends on this
assert(self.active.window.direction == .forward);
var search: PageListSearch = try .init(
self.allocator(),
self.active.window.needle,
self.needle(),
list,
history_node,
);
@ -347,7 +350,7 @@ pub const ScreenSearch = struct {
var window: SlidingWindow = try .init(
alloc,
.forward,
self.active.window.needle,
self.needle(),
);
defer window.deinit();
while (true) {