mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-10 19:29:37 +00:00
renderer: avoid allocating when there are no active links (#13258)
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. This adds an additional `self.links` iteration, but that list is usually small, the "active" check is cheap, and it breaks on the first hit. --- GPT 5.5 spotted this and wrote the test case. I came up with the tradeoffs myself and wrote the runtime code.
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user