terminal: search.Thread starting search loop

pull/9602/head
Mitchell Hashimoto 2025-11-14 17:04:31 -08:00
parent 19dfc0aa98
commit d1ad32eadd
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
1 changed files with 162 additions and 2 deletions

View File

@ -15,8 +15,12 @@ const Allocator = std.mem.Allocator;
const xev = @import("../../global.zig").xev; const xev = @import("../../global.zig").xev;
const internal_os = @import("../../os/main.zig"); const internal_os = @import("../../os/main.zig");
const BlockingQueue = @import("../../datastruct/main.zig").BlockingQueue; const BlockingQueue = @import("../../datastruct/main.zig").BlockingQueue;
const Screen = @import("../Screen.zig");
const ScreenSet = @import("../ScreenSet.zig");
const Terminal = @import("../Terminal.zig"); const Terminal = @import("../Terminal.zig");
const ScreenSearch = @import("screen.zig").ScreenSearch;
const log = std.log.scoped(.search_thread); const log = std.log.scoped(.search_thread);
/// Allocator used for some state /// Allocator used for some state
@ -38,6 +42,10 @@ wakeup_c: xev.Completion = .{},
stop: xev.Async, stop: xev.Async,
stop_c: xev.Completion = .{}, stop_c: xev.Completion = .{},
/// Search state. Starts as null and is populated when a search is
/// started (a needle is given).
search: ?Search = null,
/// The options used to initialize this thread. /// The options used to initialize this thread.
opts: Options, opts: Options,
@ -79,6 +87,8 @@ pub fn deinit(self: *Thread) void {
self.loop.deinit(); self.loop.deinit();
// Nothing can possibly access the mailbox anymore, destroy it. // Nothing can possibly access the mailbox anymore, destroy it.
self.mailbox.destroy(self.alloc); self.mailbox.destroy(self.alloc);
if (self.search) |*s| s.deinit();
} }
/// The main entrypoint for the thread. /// The main entrypoint for the thread.
@ -118,16 +128,106 @@ fn threadMain_(self: *Thread) !void {
// Run // Run
log.debug("starting search thread", .{}); log.debug("starting search thread", .{});
defer log.debug("starting search thread shutdown", .{}); defer log.debug("starting search thread shutdown", .{});
_ = try self.loop.run(.until_done);
// Unlike some of our other threads, we interleave search work
// with our xev loop so that we can try to make forward search progress
// while also listening for messages.
while (true) {
// If our loop is canceled then we drain our messages and quit.
if (self.loop.stopped()) {
while (self.mailbox.pop()) |message| {
log.debug("mailbox message ignored during shutdown={}", .{message});
}
return;
}
const s: *Search = if (self.search) |*s| s else {
// If we're not actively searching, we can block the loop
// until it does some work.
try self.loop.run(.once);
continue;
};
if (s.isComplete()) {
// If our search is complete, there's no more work to do, we
// can block until we have an xev action.
try self.loop.run(.once);
continue;
}
// Tick the search. This will trigger any event callbacks, lock
// for data loading, etc.
try s.tick(self);
// We have an active search, so we only want to process messages
// we have but otherwise return immediately so we can continue the
// search.
try self.loop.run(.no_wait);
}
} }
/// Drain the mailbox. /// Drain the mailbox.
fn drainMailbox(self: *Thread) !void { fn drainMailbox(self: *Thread) !void {
while (self.mailbox.pop()) |message| { while (self.mailbox.pop()) |message| {
log.debug("mailbox message={}", .{message}); log.debug("mailbox message={}", .{message});
switch (message) {
.change_needle => |v| try self.changeNeedle(v),
}
} }
} }
/// Change the search term to the given value.
fn changeNeedle(self: *Thread, needle: []const u8) !void {
log.debug("changing search needle to '{s}'", .{needle});
// Stop the previous search
if (self.search) |*s| {
s.deinit();
self.search = null;
}
// 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;
}
fn wakeupCallback( fn wakeupCallback(
self_: ?*Thread, self_: ?*Thread,
_: *xev.Loop, _: *xev.Loop,
@ -166,6 +266,13 @@ pub const Options = struct {
/// The terminal data to search. /// The terminal data to search.
terminal: *Terminal, terminal: *Terminal,
/// The callback for events from the search thread along with optional
/// userdata. This can be null if you don't want to receive events,
/// which could be useful for a one-time search (although, odd, you
/// should use our search structures directly then).
event_cb: ?*const fn (event: Event, userdata: ?*anyopaque) void = null,
event_userdata: ?*anyopaque = null,
}; };
/// The type used for sending messages to the thread. /// The type used for sending messages to the thread.
@ -179,12 +286,57 @@ pub const Message = union(enum) {
change_needle: []const u8, change_needle: []const u8,
}; };
/// Events that can be emitted from the search thread. The caller
/// chooses to handle these as they see fit.
pub const Event = union(enum) {
/// Nothing yet. :)
todo,
};
/// Search state.
const Search = struct {
/// The searchers for all the screens.
screens: std.EnumMap(ScreenSet.Key, ScreenSearch),
pub const empty: Search = .{
.screens = .init(.{}),
};
pub fn deinit(self: *Search) void {
var it = self.screens.iterator();
while (it.next()) |entry| entry.value.deinit();
}
/// Returns true if all searches on all screens are complete.
pub fn isComplete(self: *Search) bool {
var it = self.screens.iterator();
while (it.next()) |entry| {
switch (entry.value.state) {
.complete => {},
else => return false,
}
}
return true;
}
pub fn tick(self: *Search, thread: *Thread) !void {
// TODO
_ = self;
_ = thread;
}
};
test { test {
const alloc = testing.allocator; const alloc = testing.allocator;
var mutex: std.Thread.Mutex = .{}; var mutex: std.Thread.Mutex = .{};
var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 2 }); var t: Terminal = try .init(alloc, .{ .cols = 20, .rows = 2 });
defer t.deinit(alloc); defer t.deinit(alloc);
var stream = t.vtStream();
defer stream.deinit();
try stream.nextSlice("Hello, world");
var thread: Thread = try .init(alloc, .{ var thread: Thread = try .init(alloc, .{
.mutex = &mutex, .mutex = &mutex,
.terminal = &t, .terminal = &t,
@ -196,6 +348,14 @@ test {
threadMain, threadMain,
.{&thread}, .{&thread},
); );
// Start our search
_ = thread.mailbox.push(
.{ .change_needle = "world" },
.forever,
);
try thread.wakeup.notify();
try thread.stop.notify(); try thread.stop.notify();
os_thread.join(); os_thread.join();
} }