search: thread dispatches selection notices, messages

pull/9702/head
Mitchell Hashimoto 2025-11-25 10:17:54 -08:00
parent a2a771bb6f
commit 333dd08c97
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
4 changed files with 129 additions and 49 deletions

View File

@ -1376,6 +1376,7 @@ fn searchCallback_(
},
// Unhandled, so far.
.selected_match,
.total_matches,
.complete,
=> {},

View File

@ -43,6 +43,10 @@ pub const Untracked = struct {
self.end,
);
}
pub fn eql(self: Untracked, other: Untracked) bool {
return self.start.eql(other.start) and self.end.eql(other.end);
}
};
/// A tracked highlight is a highlight that stores its highlighted

View File

@ -19,6 +19,7 @@ const internal_os = @import("../../os/main.zig");
const BlockingQueue = @import("../../datastruct/main.zig").BlockingQueue;
const point = @import("../point.zig");
const FlattenedHighlight = @import("../highlight.zig").Flattened;
const UntrackedHighlight = @import("../highlight.zig").Untracked;
const PageList = @import("../PageList.zig");
const Screen = @import("../Screen.zig");
const ScreenSet = @import("../ScreenSet.zig");
@ -242,10 +243,23 @@ fn drainMailbox(self: *Thread) !void {
log.debug("mailbox message={}", .{message});
switch (message) {
.change_needle => |v| try self.changeNeedle(v),
.select => |v| try self.select(v),
}
}
}
fn select(self: *Thread, sel: ScreenSearch.Select) !void {
const s = if (self.search) |*s| s else return;
const screen_search = s.screens.getPtr(s.last_screen.key) orelse return;
self.opts.mutex.lock();
defer self.opts.mutex.unlock();
// The selection will trigger a selection change notification
// if it did change.
try screen_search.select(sel);
}
/// Change the search term to the given value.
fn changeNeedle(self: *Thread, needle: []const u8) !void {
log.debug("changing search needle to '{s}'", .{needle});
@ -395,6 +409,9 @@ pub const Message = union(enum) {
/// will start a search. If an existing search term is given this will
/// stop the prior search and start a new one.
change_needle: []const u8,
/// Select a search result.
select: ScreenSearch.Select,
};
/// Events that can be emitted from the search thread. The caller
@ -409,9 +426,17 @@ pub const Event = union(enum) {
/// Total matches on the current active screen have changed.
total_matches: usize,
/// Selected match changed.
selected_match: ?SelectedMatch,
/// Matches in the viewport have changed. The memory is owned by the
/// search thread and is only valid during the callback.
viewport_matches: []const FlattenedHighlight,
pub const SelectedMatch = struct {
idx: usize,
highlight: FlattenedHighlight,
};
};
/// Search state.
@ -422,11 +447,9 @@ const Search = struct {
/// The searchers for all the screens.
screens: std.EnumMap(ScreenSet.Key, ScreenSearch),
/// The last active screen
last_active_screen: ScreenSet.Key,
/// The last total matches reported.
last_total: ?usize,
/// All state related to screen switches, collected so that when
/// we switch screens it makes everything related stale, too.
last_screen: ScreenState,
/// True if we sent the complete notification yet.
last_complete: bool,
@ -434,6 +457,22 @@ const Search = struct {
/// The last viewport matches we found.
stale_viewport_matches: bool,
const ScreenState = struct {
/// Last active screen key
key: ScreenSet.Key,
/// Last notified total matches count
total: ?usize = null,
/// Last notified selected match index
selected: ?SelectedMatch = null,
const SelectedMatch = struct {
idx: usize,
highlight: UntrackedHighlight,
};
};
pub fn init(
alloc: Allocator,
needle: []const u8,
@ -448,8 +487,7 @@ const Search = struct {
return .{
.viewport = vp,
.screens = .init(.{}),
.last_active_screen = .primary,
.last_total = null,
.last_screen = .{ .key = .primary },
.last_complete = false,
.stale_viewport_matches = true,
};
@ -528,9 +566,10 @@ const Search = struct {
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
if (t.screens.active_key != self.last_screen.key) {
// The default values will force resets of a bunch of other
// state too to force recalculations and notifications.
self.last_screen = .{ .key = t.screens.active_key };
}
// Reconcile our screens with the terminal screens. Remove
@ -621,13 +660,13 @@ const Search = struct {
cb: EventCallback,
ud: ?*anyopaque,
) void {
const screen_search = self.screens.get(self.last_active_screen) orelse return;
const screen_search = self.screens.get(self.last_screen.key) orelse return;
// Check our total match data
const total = screen_search.matchesLen();
if (total != self.last_total) {
if (total != self.last_screen.total) {
log.debug("notifying total matches={}", .{total});
self.last_total = total;
self.last_screen.total = total;
cb(.{ .total_matches = total }, ud);
}
@ -666,6 +705,40 @@ const Search = struct {
cb(.{ .viewport_matches = results.items }, ud);
}
// Check our last selected match data.
if (screen_search.selected) |m| match: {
const flattened = screen_search.selectedMatch() orelse break :match;
const untracked = flattened.untracked();
if (self.last_screen.selected) |prev| {
if (prev.idx == m.idx and prev.highlight.eql(untracked)) {
// Same selection, don't update it.
break :match;
}
}
// New selection, notify!
self.last_screen.selected = .{
.idx = m.idx,
.highlight = untracked,
};
log.debug("notifying selection updated idx={}", .{m.idx});
cb(
.{ .selected_match = .{
.idx = m.idx,
.highlight = flattened,
} },
ud,
);
} else if (self.last_screen.selected != null) {
log.debug("notifying selection cleared", .{});
self.last_screen.selected = null;
cb(
.{ .selected_match = null },
ud,
);
}
// Send our complete notification if we just completed.
if (!self.last_complete and self.isComplete()) {
log.debug("notifying search complete", .{});
@ -675,40 +748,42 @@ const Search = struct {
}
};
const TestUserData = struct {
const Self = @This();
reset: std.Thread.ResetEvent = .{},
total: usize = 0,
selected: ?Event.SelectedMatch = null,
viewport: []FlattenedHighlight = &.{},
fn deinit(self: *Self) void {
for (self.viewport) |*hl| hl.deinit(testing.allocator);
testing.allocator.free(self.viewport);
}
fn callback(event: Event, userdata: ?*anyopaque) void {
const ud: *Self = @ptrCast(@alignCast(userdata.?));
switch (event) {
.quit => {},
.complete => ud.reset.set(),
.total_matches => |v| ud.total = v,
.selected_match => |v| ud.selected = v,
.viewport_matches => |v| {
for (ud.viewport) |*hl| hl.deinit(testing.allocator);
testing.allocator.free(ud.viewport);
ud.viewport = testing.allocator.alloc(
FlattenedHighlight,
v.len,
) catch unreachable;
for (ud.viewport, v) |*dst, src| {
dst.* = src.clone(testing.allocator) catch unreachable;
}
},
}
}
};
test {
const UserData = struct {
const Self = @This();
reset: std.Thread.ResetEvent = .{},
total: usize = 0,
viewport: []FlattenedHighlight = &.{},
fn deinit(self: *Self) void {
for (self.viewport) |*hl| hl.deinit(testing.allocator);
testing.allocator.free(self.viewport);
}
fn callback(event: Event, userdata: ?*anyopaque) void {
const ud: *Self = @ptrCast(@alignCast(userdata.?));
switch (event) {
.quit => {},
.complete => ud.reset.set(),
.total_matches => |v| ud.total = v,
.viewport_matches => |v| {
for (ud.viewport) |*hl| hl.deinit(testing.allocator);
testing.allocator.free(ud.viewport);
ud.viewport = testing.allocator.alloc(
FlattenedHighlight,
v.len,
) catch unreachable;
for (ud.viewport, v) |*dst, src| {
dst.* = src.clone(testing.allocator) catch unreachable;
}
},
}
}
};
const alloc = testing.allocator;
var mutex: std.Thread.Mutex = .{};
var t: Terminal = try .init(alloc, .{ .cols = 20, .rows = 2 });
@ -718,12 +793,12 @@ test {
defer stream.deinit();
try stream.nextSlice("Hello, world");
var ud: UserData = .{};
var ud: TestUserData = .{};
defer ud.deinit();
var thread: Thread = try .init(alloc, .{
.mutex = &mutex,
.terminal = &t,
.event_cb = &UserData.callback,
.event_cb = &TestUserData.callback,
.event_userdata = &ud,
});
defer thread.deinit();

View File

@ -57,7 +57,7 @@ pub const ScreenSearch = struct {
history_results: std.ArrayList(FlattenedHighlight),
active_results: std.ArrayList(FlattenedHighlight),
const SelectedMatch = struct {
pub const SelectedMatch = struct {
/// Index from the end of the match list (0 = most recent match)
idx: usize,