render viewport matches

pull/9687/head
Mitchell Hashimoto 2025-11-24 12:26:59 -08:00
parent 6c8ffb5fc1
commit dd9ed531ad
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
2 changed files with 92 additions and 1 deletions

View File

@ -1191,6 +1191,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
log.warn("error searching for regex links err={}", .{err});
};
// Clear our highlight state and update.
if (self.search_matches_dirty or self.terminal_state.dirty != .false) {
for (self.terminal_state.row_data.items(.highlights)) |*highlights| {
highlights.clearRetainingCapacity();
}
if (self.search_matches) |m| {
self.terminal_state.updateHighlightsFlattened(
self.alloc,
m.matches,
) catch |err| {
// Not a critical error, we just won't show highlights.
log.warn("error updating search highlights err={}", .{err});
};
}
}
// Build our GPU cells
try self.rebuildCells(
critical.preedit,
@ -2366,6 +2383,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const row_cells = row_data.items(.cells);
const row_dirty = row_data.items(.dirty);
const row_selection = row_data.items(.selection);
const row_highlights = row_data.items(.highlights);
// If our cell contents buffer is shorter than the screen viewport,
// we render the rows that fit, starting from the bottom. If instead
@ -2381,7 +2399,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
row_cells[0..row_len],
row_dirty[0..row_len],
row_selection[0..row_len],
) |y_usize, row, *cells, *dirty, selection| {
row_highlights[0..row_len],
) |y_usize, row, *cells, *dirty, selection, highlights| {
const y: terminal.size.CellCountInt = @intCast(y_usize);
if (!rebuild) {
@ -2526,6 +2545,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// True if this cell is selected
const selected: bool = 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 true;
}
}
const sel = selection orelse break :selected false;
const x_compare = if (wide == .spacer_tail)
x -| 1

View File

@ -5,6 +5,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const fastmem = @import("../fastmem.zig");
const color = @import("color.zig");
const cursor = @import("cursor.zig");
const highlight = @import("highlight.zig");
const point = @import("point.zig");
const size = @import("size.zig");
const page = @import("page.zig");
@ -191,6 +192,10 @@ 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),
};
pub const Cell = struct {
@ -348,6 +353,7 @@ pub const RenderState = struct {
.cells = .empty,
.dirty = true,
.selection = null,
.highlights = .empty,
});
}
} else {
@ -630,6 +636,63 @@ pub const RenderState = struct {
s.dirty = .{};
}
/// Update the highlights in the render state from the given flattened
/// highlights. Because this uses flattened highlights, it does not require
/// reading from the terminal state so it should be done outside of
/// any critical sections.
///
/// This will not clear any previous highlights, so the caller must
/// manually clear them if desired.
pub fn updateHighlightsFlattened(
self: *RenderState,
alloc: Allocator,
hls: []const highlight.Flattened,
) Allocator.Error!void {
// Fast path, we have no highlights!
if (hls.len == 0) return;
// This is, admittedly, horrendous. This is some low hanging fruit
// to optimize. In my defense, screens are usually small, the number
// of highlights is usually small, and this only happens on the
// viewport outside of a locked area. Still, I'd love to see this
// improved someday.
const row_data = self.row_data.slice();
const row_arenas = row_data.items(.arena);
const row_pins = row_data.items(.pin);
const row_highlights_slice = row_data.items(.highlights);
for (
row_arenas,
row_pins,
row_highlights_slice,
) |*row_arena, row_pin, *row_highlights| {
for (hls) |hl| {
const chunks_slice = hl.chunks.slice();
const nodes = chunks_slice.items(.node);
const starts = chunks_slice.items(.start);
const ends = chunks_slice.items(.end);
for (0.., nodes) |i, node| {
// If this node doesn't match or we're not within
// the row range, skip it.
if (node != row_pin.node or
row_pin.y < starts[i] or
row_pin.y >= ends[i]) continue;
// We're a match!
var arena = row_arena.promote(alloc);
defer row_arena.* = arena.state;
const arena_alloc = arena.allocator();
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,
},
);
}
}
}
}
pub const StringMap = std.ArrayListUnmanaged(point.Coordinate);
/// Convert the current render state contents to a UTF-8 encoded