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. // for data loading, etc.
switch (s.tick()) { switch (s.tick()) {
// We're complete now when we were not before. Notify! // We're complete now when we were not before. Notify!
.complete => if (self.opts.event_cb) |cb| { .complete => {},
cb(.complete, self.opts.event_userdata);
},
// Forward progress was made. // Forward progress was made.
.progress => {}, .progress => {},
// All searches are blocked. Let's grab the lock and feed data. // All searches are blocked. Let's grab the lock and feed data.
.blocked => { .blocked => {
try s.feed(self.opts.mutex, self.opts.terminal); self.opts.mutex.lock();
defer self.opts.mutex.unlock();
// Feeding can result in completion if there is no more try s.feed(
// data to feed. If we transitioned to complete, notify! self.alloc,
if (self.opts.event_cb) |cb| { self.opts.terminal,
if (s.isComplete()) cb( );
.complete,
self.opts.event_userdata,
);
}
}, },
} }
@ -189,6 +183,13 @@ fn threadMain_(self: *Thread) !void {
cb, cb,
self.opts.event_userdata, 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 // 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. // No needle means stop the search.
if (needle.len == 0) return; 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. // We need to grab the terminal lock to setup our search state.
self.opts.mutex.lock(); self.opts.mutex.lock();
defer self.opts.mutex.unlock(); defer self.opts.mutex.unlock();
const t: *Terminal = self.opts.terminal; const t: *Terminal = self.opts.terminal;
self.search = try .init(self.alloc, needle, t);
// 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;
} }
fn wakeupCallback( fn wakeupCallback(
@ -340,11 +310,27 @@ const Search = struct {
/// The last total matches reported. /// The last total matches reported.
last_total: ?usize, last_total: ?usize,
pub const empty: Search = .{ pub fn init(
.screens = .init(.{}), alloc: Allocator,
.last_active_screen = .primary, needle: []const u8,
.last_total = null, 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 { pub fn deinit(self: *Search) void {
var it = self.screens.iterator(); var it = self.screens.iterator();
@ -412,16 +398,63 @@ const Search = struct {
/// Grab the mutex and update any state that requires it, such as /// Grab the mutex and update any state that requires it, such as
/// feeding additional data to the searches or updating the active screen. /// feeding additional data to the searches or updating the active screen.
pub fn feed(self: *Search, mutex: *Mutex, t: *Terminal) !void { pub fn feed(
mutex.lock(); self: *Search,
defer mutex.unlock(); alloc: Allocator,
t: *Terminal,
) !void {
// Update our active screen // Update our active screen
if (t.screens.active_key != self.last_active_screen) { if (t.screens.active_key != self.last_active_screen) {
self.last_active_screen = t.screens.active_key; self.last_active_screen = t.screens.active_key;
self.last_total = null; // force notification 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 // Feed data
var it = self.screens.iterator(); var it = self.screens.iterator();
while (it.next()) |entry| { while (it.next()) |entry| {

View File

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