Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323const std = @import("std");const assert = std.debug.assert;const Allocator = std.mem.Allocator;const oni = @import("oniguruma");const inputpkg = @import("../input.zig");const terminal = @import("../terminal/main.zig");const point = terminal.point;const Screen = terminal.Screen;const Terminal = terminal.Terminal;
const log = std.log.scoped(.renderer_link);
/// The link configuration needed for renderers.pub const Link = struct { /// The regular expression to match the link against. regex: oni.Regex,
/// The situations in which the link should be highlighted. highlight: inputpkg.Link.Highlight,
pub fn deinit(self: *Link) void { self.regex.deinit(); }};
/// A set of links. This provides a higher level API for renderers/// to match against a viewport and determine if cells are part of/// a link.pub const Set = struct { links: []Link,
/// Returns the slice of links from the configuration. pub fn fromConfig( alloc: Allocator, config: []const inputpkg.Link, ) !Set { var links: std.ArrayList(Link) = .empty; defer links.deinit(alloc);
for (config) |link| { var regex = try link.oniRegex(); errdefer regex.deinit(); try links.append(alloc, .{ .regex = regex, .highlight = link.highlight, }); }
return .{ .links = try links.toOwnedSlice(alloc) }; }
pub fn deinit(self: *Set, alloc: Allocator) void { for (self.links) |*link| link.deinit(); alloc.free(self.links); }
/// Fills matches with the matches from regex link matches. pub fn renderCellMap( self: *const Set, alloc: Allocator, result: *terminal.RenderState.CellSet, render_state: *const terminal.RenderState, mouse_viewport: ?point.Coordinate, mouse_mods: inputpkg.Mods, ) !void { // Fast path, not very likely since we have default links. if (self.links.len == 0) return;
// Convert our render state to a string + byte map. var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); var map: terminal.RenderState.StringMap = .empty; defer map.deinit(alloc); try render_state.string(&builder.writer, .{ .alloc = alloc, .map = &map, });
const str = builder.writer.buffered();
// 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; }, }
var offset: usize = 0; while (offset < str.len) { var region = link.regex.search( str[offset..], .{}, ) catch |err| switch (err) { error.Mismatch => break, else => return err, }; defer region.deinit();
// We have a match! const offset_start: usize = @intCast(region.starts()[0]); const offset_end: usize = @intCast(region.ends()[0]); const start = offset + offset_start; const end = offset + offset_end;
// Increment our offset by the number of bytes in the match. // We defer this so that we can return the match before // modifying the offset. defer offset = end;
switch (link.highlight) { .always, .always_mods => {}, .hover, .hover_mods => if (mouse_viewport) |vp| { for (map.items[start..end]) |pt| { if (pt.eql(vp)) break; } else continue; } else continue, }
// Record the match for (map.items[start..end]) |pt| { try result.put(alloc, pt, {}); } } } }};
test "renderCellMap" { 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);
// Get a set var set = try Set.fromConfig(alloc, &.{ .{ .regex = "AB", .action = .{ .open = {} }, .highlight = .{ .always = {} }, },
.{ .regex = "EF", .action = .{ .open = {} }, .highlight = .{ .always = {} }, }, }); defer set.deinit(alloc);
// Get our matches var result: terminal.RenderState.CellSet = .empty; defer result.deinit(alloc); try set.renderCellMap( alloc, &result, &state, null, .{}, ); try testing.expect(!result.contains(.{ .x = 0, .y = 0 })); try testing.expect(result.contains(.{ .x = 1, .y = 0 })); try testing.expect(result.contains(.{ .x = 2, .y = 0 })); try testing.expect(!result.contains(.{ .x = 3, .y = 0 })); try testing.expect(result.contains(.{ .x = 1, .y = 1 })); try testing.expect(!result.contains(.{ .x = 1, .y = 2 }));}
test "renderCellMap hover links" { 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);
// Get a set var set = try Set.fromConfig(alloc, &.{ .{ .regex = "AB", .action = .{ .open = {} }, .highlight = .{ .hover = {} }, },
.{ .regex = "EF", .action = .{ .open = {} }, .highlight = .{ .always = {} }, }, }); defer set.deinit(alloc);
// Not hovering over the first link { var result: terminal.RenderState.CellSet = .empty; defer result.deinit(alloc); try set.renderCellMap( alloc, &result, &state, null, .{}, );
// Test our matches try testing.expect(!result.contains(.{ .x = 0, .y = 0 })); try testing.expect(!result.contains(.{ .x = 1, .y = 0 })); try testing.expect(!result.contains(.{ .x = 2, .y = 0 })); try testing.expect(!result.contains(.{ .x = 3, .y = 0 })); try testing.expect(result.contains(.{ .x = 1, .y = 1 })); try testing.expect(!result.contains(.{ .x = 1, .y = 2 })); }
// Hovering over the first link { var result: terminal.RenderState.CellSet = .empty; defer result.deinit(alloc); try set.renderCellMap( alloc, &result, &state, .{ .x = 1, .y = 0 }, .{}, );
// Test our matches try testing.expect(!result.contains(.{ .x = 0, .y = 0 })); try testing.expect(result.contains(.{ .x = 1, .y = 0 })); try testing.expect(result.contains(.{ .x = 2, .y = 0 })); try testing.expect(!result.contains(.{ .x = 3, .y = 0 })); try testing.expect(result.contains(.{ .x = 1, .y = 1 })); try testing.expect(!result.contains(.{ .x = 1, .y = 2 })); }}
test "renderCellMap mods no match" { 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);
// Get a set var set = try Set.fromConfig(alloc, &.{ .{ .regex = "AB", .action = .{ .open = {} }, .highlight = .{ .always = {} }, },
.{ .regex = "EF", .action = .{ .open = {} }, .highlight = .{ .always_mods = .{ .ctrl = true } }, }, }); defer set.deinit(alloc);
// Get our matches var result: terminal.RenderState.CellSet = .empty; defer result.deinit(alloc); try set.renderCellMap( alloc, &result, &state, null, .{}, );
// Test our matches try testing.expect(!result.contains(.{ .x = 0, .y = 0 })); try testing.expect(result.contains(.{ .x = 1, .y = 0 })); try testing.expect(result.contains(.{ .x = 2, .y = 0 })); try testing.expect(!result.contains(.{ .x = 3, .y = 0 })); try testing.expect(!result.contains(.{ .x = 1, .y = 1 })); try testing.expect(!result.contains(.{ .x = 1, .y = 2 }));}