renderer: hook up search selection match highlighting

pull/9702/head
Mitchell Hashimoto 2025-11-25 10:31:34 -08:00
parent 333dd08c97
commit 880db9fdd0
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
6 changed files with 122 additions and 9 deletions

View File

@ -1363,6 +1363,32 @@ fn searchCallback_(
try self.renderer_thread.wakeup.notify();
},
.selected_match => |selected_| {
if (selected_) |sel| {
// Copy the flattened match.
var arena: ArenaAllocator = .init(self.alloc);
errdefer arena.deinit();
const alloc = arena.allocator();
const match = try sel.highlight.clone(alloc);
_ = self.renderer_thread.mailbox.push(
.{ .search_selected_match = .{
.arena = arena,
.match = match,
} },
.forever,
);
} else {
// Reset our selected match
_ = self.renderer_thread.mailbox.push(
.{ .search_selected_match = null },
.forever,
);
}
try self.renderer_thread.wakeup.notify();
},
// When we quit, tell our renderer to reset any search state.
.quit => {
_ = self.renderer_thread.mailbox.push(
@ -1376,7 +1402,6 @@ fn searchCallback_(
},
// Unhandled, so far.
.selected_match,
.total_matches,
.complete,
=> {},

View File

@ -988,10 +988,25 @@ palette: Palette = .{},
/// - "cell-foreground" to match the cell foreground color
/// - "cell-background" to match the cell background color
///
/// The default value is
/// The default value is black text on a golden yellow background.
@"search-foreground": TerminalColor = .{ .color = .{ .r = 0, .g = 0, .b = 0 } },
@"search-background": TerminalColor = .{ .color = .{ .r = 0xFF, .g = 0xE0, .b = 0x82 } },
/// The foreground and background color for the currently selected search match.
/// This is the focused match that will be jumped to when using next/previous
/// search navigation.
///
/// Valid values:
///
/// - Hex (`#RRGGBB` or `RRGGBB`)
/// - Named X11 color
/// - "cell-foreground" to match the cell foreground color
/// - "cell-background" to match the cell background color
///
/// The default value is black text on a bright orange background.
@"search-selected-foreground": TerminalColor = .{ .color = .{ .r = 0, .g = 0, .b = 0 } },
@"search-selected-background": TerminalColor = .{ .color = .{ .r = 0xFE, .g = 0xA6, .b = 0x2B } },
/// The command to run, usually a shell. If this is not an absolute path, it'll
/// be looked up in the `PATH`. If this is not set, a default will be looked up
/// from your system. The rules for the default lookup are:

View File

@ -459,6 +459,14 @@ fn drainMailbox(self: *Thread) !void {
self.renderer.search_matches_dirty = true;
},
.search_selected_match => |v| {
// Note we don't free the new value because we expect our
// allocators to match.
if (self.renderer.search_selected_match) |*m| m.arena.deinit();
self.renderer.search_selected_match = v;
self.renderer.search_matches_dirty = true;
},
.inspector => |v| self.flags.has_inspector = v,
.macos_display_id => |v| {

View File

@ -130,6 +130,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// Note that the selections MAY BE INVALID (point to PageList nodes
/// that do not exist anymore). These must be validated prior to use.
search_matches: ?renderer.Message.SearchMatches,
search_selected_match: ?renderer.Message.SearchMatch,
search_matches_dirty: bool,
/// The current set of cells to render. This is rebuilt on every frame
@ -222,6 +223,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// a large screen.
terminal_state_frame_count: usize = 0,
const HighlightTag = enum(u8) {
search_match,
search_match_selected,
};
/// Swap chain which maintains multiple copies of the state needed to
/// render a frame, so that we can start building the next frame while
/// the previous frame is still being processed on the GPU.
@ -539,6 +545,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
selection_foreground: ?configpkg.Config.TerminalColor,
search_background: configpkg.Config.TerminalColor,
search_foreground: configpkg.Config.TerminalColor,
search_selected_background: configpkg.Config.TerminalColor,
search_selected_foreground: configpkg.Config.TerminalColor,
bold_color: ?configpkg.BoldColor,
faint_opacity: u8,
min_contrast: f32,
@ -612,6 +620,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.selection_foreground = config.@"selection-foreground",
.search_background = config.@"search-background",
.search_foreground = config.@"search-foreground",
.search_selected_background = config.@"search-selected-background",
.search_selected_foreground = config.@"search-selected-foreground",
.custom_shaders = custom_shaders,
.bg_image = bg_image,
@ -687,6 +697,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.scrollbar = .zero,
.scrollbar_dirty = false,
.search_matches = null,
.search_selected_match = null,
.search_matches_dirty = false,
// Render state
@ -760,6 +771,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
pub fn deinit(self: *Self) void {
self.terminal_state.deinit(self.alloc);
if (self.search_selected_match) |*m| m.arena.deinit();
if (self.search_matches) |*m| m.arena.deinit();
self.swap_chain.deinit();
@ -1209,9 +1221,24 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
highlights.clearRetainingCapacity();
}
// NOTE: The order below matters. Highlights added earlier
// will take priority.
if (self.search_selected_match) |m| {
self.terminal_state.updateHighlightsFlattened(
self.alloc,
@intFromEnum(HighlightTag.search_match_selected),
(&m.match)[0..1],
) catch |err| {
// Not a critical error, we just won't show highlights.
log.warn("error updating search selected highlight err={}", .{err});
};
}
if (self.search_matches) |m| {
self.terminal_state.updateHighlightsFlattened(
self.alloc,
@intFromEnum(HighlightTag.search_match),
m.matches,
) catch |err| {
// Not a critical error, we just won't show highlights.
@ -2560,13 +2587,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
false,
selection,
search,
search_selected,
} = selected: {
// If we're highlighted, then we're selected. In the
// future we want to use a different style for this
// but this to get started.
for (highlights.items) |hl| {
if (x >= hl[0] and x <= hl[1]) {
break :selected .search;
if (x >= hl.range[0] and x <= hl.range[1]) {
const tag: HighlightTag = @enumFromInt(hl.tag);
break :selected switch (tag) {
.search_match => .search,
.search_match_selected => .search_selected,
};
}
}
@ -2614,6 +2646,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.@"cell-background" => if (style.flags.inverse) fg_style else bg_style,
},
.search_selected => switch (self.config.search_selected_background) {
.color => |color| color.toTerminalRGB(),
.@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style,
.@"cell-background" => if (style.flags.inverse) fg_style else bg_style,
},
// Not selected
.false => if (style.flags.inverse != isCovering(cell.codepoint()))
// Two cases cause us to invert (use the fg color as the bg)
@ -2652,6 +2690,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.@"cell-background" => if (style.flags.inverse) fg_style else final_bg,
},
.search_selected => switch (self.config.search_selected_foreground) {
.color => |color| color.toTerminalRGB(),
.@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style,
.@"cell-background" => if (style.flags.inverse) fg_style else final_bg,
},
.false => if (style.flags.inverse)
final_bg
else

View File

@ -58,6 +58,10 @@ pub const Message = union(enum) {
/// viewport. The renderer must handle this gracefully.
search_viewport_matches: SearchMatches,
/// The selected match from the search thread. May be null to indicate
/// no match currently.
search_selected_match: ?SearchMatch,
/// Activate or deactivate the inspector.
inspector: bool,
@ -69,6 +73,11 @@ pub const Message = union(enum) {
matches: []const terminal.highlight.Flattened,
};
pub const SearchMatch = struct {
arena: ArenaAllocator,
match: terminal.highlight.Flattened,
};
/// Initialize a change_config message.
pub fn initChangeConfig(alloc: Allocator, config: *const configpkg.Config) !Message {
const thread_ptr = try alloc.create(renderer.Thread.DerivedConfig);

View File

@ -193,9 +193,17 @@ pub const RenderState = struct {
/// The x range of the selection within this row.
selection: ?[2]size.CellCountInt,
/// The x ranges of highlights within this row. Highlights are
/// applied after the update by calling `updateHighlights`.
highlights: std.ArrayList([2]size.CellCountInt),
/// The highlights within this row.
highlights: std.ArrayList(Highlight),
};
pub const Highlight = struct {
/// A special tag that can be used by the caller to differentiate
/// different highlight types. The value is opaque to the RenderState.
tag: u8,
/// The x ranges of highlights within this row.
range: [2]size.CellCountInt,
};
pub const Cell = struct {
@ -646,6 +654,7 @@ pub const RenderState = struct {
pub fn updateHighlightsFlattened(
self: *RenderState,
alloc: Allocator,
tag: u8,
hls: []const highlight.Flattened,
) Allocator.Error!void {
// Fast path, we have no highlights!
@ -691,8 +700,11 @@ pub const RenderState = struct {
try row_highlights.append(
arena_alloc,
.{
if (i == 0) hl.top_x else 0,
if (i == nodes.len - 1) hl.bot_x else self.cols - 1,
.tag = tag,
.range = .{
if (i == 0) hl.top_x else 0,
if (i == nodes.len - 1) hl.bot_x else self.cols - 1,
},
},
);