diff --git a/src/renderer/link.zig b/src/renderer/link.zig index c5de61574..11ecc87ae 100644 --- a/src/renderer/link.zig +++ b/src/renderer/link.zig @@ -21,6 +21,20 @@ pub const Link = struct { pub fn deinit(self: *Link) void { self.regex.deinit(); } + + /// Returns true if this link's highlight condition matches the given mouse state. + fn active( + self: *const Link, + mouse_viewport: ?point.Coordinate, + mouse_mods: inputpkg.Mods, + ) bool { + return switch (self.highlight) { + .always => true, + .always_mods => |v| mouse_mods.equal(v), + .hover => mouse_viewport != null, + .hover_mods => |v| mouse_viewport != null and mouse_mods.equal(v), + }; + } }; /// A set of links. This provides a higher level API for renderers @@ -66,6 +80,14 @@ pub const Set = struct { // Fast path, not very likely since we have default links. if (self.links.len == 0) return; + // Determine if any links are active before building the string and + // byte-to-cell map. Those buffers scale with viewport size and this + // function runs during frame updates, so avoid allocating them when + // the current mouse/modifier state can't highlight any regex links. + for (self.links) |*link| { + if (link.active(mouse_viewport, mouse_mods)) break; + } else return; + // Convert our render state to a string + byte map. var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); @@ -80,20 +102,7 @@ pub const Set = struct { // Go through each link and see if we have any matches. for (self.links) |*link| { - // Determine if our highlight conditions are met. We use a - // switch here instead of an if so that we can get a compile - // error if any other conditions are added. - switch (link.highlight) { - .always => {}, - .always_mods => |v| if (!mouse_mods.equal(v)) continue, - - // We check the hover points later. - .hover => if (mouse_viewport == null) continue, - .hover_mods => |v| { - if (mouse_viewport == null) continue; - if (!mouse_mods.equal(v)) continue; - }, - } + if (!link.active(mouse_viewport, mouse_mods)) continue; var offset: usize = 0; while (offset < str.len) { @@ -266,6 +275,65 @@ test "renderCellMap hover links" { } } +test "renderCellMap inactive links don't allocate" { + const testing = std.testing; + const alloc = testing.allocator; + + var t: terminal.Terminal = try .init(alloc, .{ + .cols = 5, + .rows = 3, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + const str = "1ABCD2EFGH\r\n3IJKL"; + s.nextSlice(str); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + var set = try Set.fromConfig(alloc, &.{ + .{ + .regex = "AB", + .action = .{ .open = {} }, + .highlight = .{ .hover = {} }, + }, + + .{ + .regex = "EF", + .action = .{ .open = {} }, + .highlight = .{ .always_mods = .{ .ctrl = true } }, + }, + + .{ + .regex = "IJ", + .action = .{ .open = {} }, + .highlight = .{ .hover_mods = .{ .shift = true } }, + }, + }); + defer set.deinit(alloc); + + var failing = std.testing.FailingAllocator.init( + alloc, + .{ .fail_index = 0 }, + ); + const failing_alloc = failing.allocator(); + + var result: terminal.RenderState.CellSet = .empty; + defer result.deinit(failing_alloc); + try set.renderCellMap( + failing_alloc, + &result, + &state, + null, + .{}, + ); + + try testing.expectEqual(@as(usize, 0), result.count()); +} + test "renderCellMap mods no match" { const testing = std.testing; const alloc = testing.allocator;