`search` binding action starts a search thread on surface

pull/9687/head
Mitchell Hashimoto 2025-11-15 20:02:35 -08:00
parent 6623c20c2d
commit e49f4a6dbc
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
4 changed files with 84 additions and 0 deletions

View File

@ -155,6 +155,9 @@ selection_scroll_active: bool = false,
/// the wall clock time that has elapsed between timestamps. /// the wall clock time that has elapsed between timestamps.
command_timer: ?std.time.Instant = null, command_timer: ?std.time.Instant = null,
/// Search state
search: ?Search = null,
/// The effect of an input event. This can be used by callers to take /// The effect of an input event. This can be used by callers to take
/// the appropriate action after an input event. For example, key /// the appropriate action after an input event. For example, key
/// input can be forwarded to the OS for further processing if it /// input can be forwarded to the OS for further processing if it
@ -174,6 +177,26 @@ pub const InputEffect = enum {
closed, closed,
}; };
/// The search state for the surface.
const Search = struct {
state: terminal.search.Thread,
thread: std.Thread,
pub fn deinit(self: *Search) void {
// Notify the thread to stop
self.state.stop.notify() catch |err| log.err(
"error notifying search thread to stop, may stall err={}",
.{err},
);
// Wait for the OS thread to quit
self.thread.join();
// Now it is safe to deinit the state
self.state.deinit();
}
};
/// Mouse state for the surface. /// Mouse state for the surface.
const Mouse = struct { const Mouse = struct {
/// The last tracked mouse button state by button. /// The last tracked mouse button state by button.
@ -728,6 +751,9 @@ pub fn init(
} }
pub fn deinit(self: *Surface) void { pub fn deinit(self: *Surface) void {
// Stop search thread
if (self.search) |*s| s.deinit();
// Stop rendering thread // Stop rendering thread
{ {
self.renderer_thread.stop.notify() catch |err| self.renderer_thread.stop.notify() catch |err|
@ -1301,6 +1327,12 @@ fn reportColorScheme(self: *Surface, force: bool) void {
self.io.queueMessage(.{ .write_stable = output }, .unlocked); self.io.queueMessage(.{ .write_stable = output }, .unlocked);
} }
fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void {
const self: *Surface = @ptrCast(@alignCast(ud.?));
_ = self;
_ = event;
}
/// Call this when modifiers change. This is safe to call even if modifiers /// Call this when modifiers change. This is safe to call even if modifiers
/// match the previous state. /// match the previous state.
/// ///
@ -4770,6 +4802,49 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
self.renderer_state.terminal.fullReset(); self.renderer_state.terminal.fullReset();
}, },
.search => |text| search: {
const s: *Search = if (self.search) |*s| s else init: {
// If we're stopping the search and we had no prior search,
// then there is nothing to do.
if (text.len == 0) break :search;
// We need to assign directly to self.search because we need
// a stable pointer back to the thread state.
self.search = .{
.state = try .init(self.alloc, .{
.mutex = self.renderer_state.mutex,
.terminal = self.renderer_state.terminal,
.event_cb = &searchCallback,
.event_userdata = self,
}),
.thread = undefined,
};
const s: *Search = &self.search.?;
errdefer s.state.deinit();
s.thread = try .spawn(
.{},
terminal.search.Thread.threadMain,
.{&s.state},
);
s.thread.setName("search") catch {};
break :init s;
};
// Zero-length text means stop searching.
if (text.len == 0) {
s.deinit();
self.search = null;
break :search;
}
_ = s.state.mailbox.push(
.{ .change_needle = text },
.forever,
);
},
.copy_to_clipboard => |format| { .copy_to_clipboard => |format| {
// We can read from the renderer state without holding // We can read from the renderer state without holding
// the lock because only we will write to this field. // the lock because only we will write to this field.

View File

@ -332,6 +332,10 @@ pub const Action = union(enum) {
/// to 14.5 points. /// to 14.5 points.
set_font_size: f32, set_font_size: f32,
/// Start a search for the given text. If the text is empty, then
/// the search is canceled. If a previous search is active, it is replaced.
search: []const u8,
/// Clear the screen and all scrollback. /// Clear the screen and all scrollback.
clear_screen, clear_screen,
@ -1152,6 +1156,7 @@ pub const Action = union(enum) {
.esc, .esc,
.text, .text,
.cursor_key, .cursor_key,
.search,
.reset, .reset,
.copy_to_clipboard, .copy_to_clipboard,
.copy_url_to_clipboard, .copy_url_to_clipboard,

View File

@ -604,6 +604,7 @@ fn actionCommands(action: Action.Key) []const Command {
.csi, .csi,
.esc, .esc,
.cursor_key, .cursor_key,
.search,
.set_font_size, .set_font_size,
.scroll_to_row, .scroll_to_row,
.scroll_page_fractional, .scroll_page_fractional,

View File

@ -591,6 +591,7 @@ const Search = struct {
// Check our total match data // Check our total match data
const total = screen_search.matchesLen(); const total = screen_search.matchesLen();
if (total != self.last_total) { if (total != self.last_total) {
log.debug("notifying total matches={}", .{total});
self.last_total = total; self.last_total = total;
cb(.{ .total_matches = total }, ud); cb(.{ .total_matches = total }, ud);
} }
@ -626,11 +627,13 @@ const Search = struct {
}; };
} }
log.debug("notifying viewport matches len={}", .{results.items.len});
cb(.{ .viewport_matches = results.items }, ud); cb(.{ .viewport_matches = results.items }, ud);
} }
// Send our complete notification if we just completed. // Send our complete notification if we just completed.
if (!self.last_complete and self.isComplete()) { if (!self.last_complete and self.isComplete()) {
log.debug("notifying search complete", .{});
self.last_complete = true; self.last_complete = true;
cb(.complete, ud); cb(.complete, ud);
} }