From 041c7795127451d3fbe02d565e53d2bf38a0807d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 4 Jul 2024 18:58:21 -0700 Subject: [PATCH] renderer: matchSet matches OSC8 --- src/renderer/link.zig | 183 +++++++++++++++++++++++++++++++++++++++++- src/terminal/page.zig | 2 +- 2 files changed, 181 insertions(+), 4 deletions(-) diff --git a/src/renderer/link.zig b/src/renderer/link.zig index e6c7f6ba0..417fcfcf9 100644 --- a/src/renderer/link.zig +++ b/src/renderer/link.zig @@ -6,6 +6,7 @@ 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); @@ -79,10 +80,125 @@ pub const Set = struct { var matches = std.ArrayList(terminal.Selection).init(alloc); defer matches.deinit(); + // If our mouse is over an OSC8 link, then we can skip the regex + // matches below since OSC8 takes priority. + try self.matchSetFromOSC8( + alloc, + &matches, + screen, + mouse_pin, + mouse_mods, + ); + + // If we have no matches then we can try the regex matches. + if (matches.items.len == 0) { + try self.matchSetFromLinks( + alloc, + &matches, + screen, + mouse_pin, + mouse_mods, + ); + } + + return .{ .matches = try matches.toOwnedSlice() }; + } + + fn matchSetFromOSC8( + self: *const Set, + alloc: Allocator, + matches: *std.ArrayList(terminal.Selection), + screen: *Screen, + mouse_pin: terminal.Pin, + mouse_mods: inputpkg.Mods, + ) !void { + _ = alloc; + _ = self; + + // If the right mods aren't pressed, then we can't match. + if (!mouse_mods.equal(inputpkg.ctrlOrSuper(.{}))) return; + + // Check if the cell the mouse is over is an OSC8 hyperlink + const mouse_cell = mouse_pin.rowAndCell().cell; + if (!mouse_cell.hyperlink) return; + + // Get our hyperlink entry + const page = &mouse_pin.page.data; + const link_id = page.lookupHyperlink(mouse_cell) orelse { + log.warn("failed to find hyperlink for cell", .{}); + return; + }; + + // Go through every row and find matching hyperlinks for the given ID. + // Note the link ID is not the same as the OSC8 ID parameter. But + // we hash hyperlinks by their contents which should achieve the same + // thing so we can use the ID as a key. + var current: ?terminal.Selection = null; + var row_it = screen.pages.getTopLeft(.viewport).rowIterator(.right_down, null); + while (row_it.next()) |row_pin| { + const row = row_pin.rowAndCell().row; + + // If the row doesn't have any hyperlinks then we're done + // building our matching selection. + if (!row.hyperlink) { + if (current) |sel| { + try matches.append(sel); + current = null; + } + + continue; + } + + // We have hyperlinks, look for our own matching hyperlink. + for (row_pin.cells(.right), 0..) |*cell, x| { + const match = match: { + if (cell.hyperlink) { + if (row_pin.page.data.lookupHyperlink(cell)) |cell_link_id| { + break :match cell_link_id == link_id; + } + } + break :match false; + }; + + // If we have a match, extend our selection or start a new + // selection. + if (match) { + const cell_pin = row_pin.right(x); + if (current) |*sel| { + sel.endPtr().* = cell_pin; + } else { + current = terminal.Selection.init( + cell_pin, + cell_pin, + false, + ); + } + + continue; + } + + // No match, if we have a current selection then complete it. + if (current) |sel| { + try matches.append(sel); + current = null; + } + } + } + } + + /// Fills matches with the matches from regex link matches. + fn matchSetFromLinks( + self: *const Set, + alloc: Allocator, + matches: *std.ArrayList(terminal.Selection), + screen: *Screen, + mouse_pin: terminal.Pin, + mouse_mods: inputpkg.Mods, + ) !void { // Iterate over all the visible lines. var lineIter = screen.lineIterator(screen.pages.pin(.{ .viewport = .{}, - }) orelse return .{}); + }) orelse return); while (lineIter.next()) |line_sel| { const strmap: terminal.StringMap = strmap: { var strmap: terminal.StringMap = undefined; @@ -141,8 +257,6 @@ pub const Set = struct { } } } - - return .{ .matches = try matches.toOwnedSlice() }; } }; @@ -391,3 +505,66 @@ test "matchset mods no match" { .y = 2, } }).?)); } + +test "matchset osc8" { + const testing = std.testing; + const alloc = testing.allocator; + + // Initialize our terminal + var t = try Terminal.init(alloc, .{ .cols = 10, .rows = 10 }); + defer t.deinit(alloc); + const s = &t.screen; + + try t.printString("ABC"); + try t.screen.startHyperlink("http://example.com", null); + try t.printString("123"); + t.screen.endHyperlink(); + + // Get a set + var set = try Set.fromConfig(alloc, &.{}); + defer set.deinit(alloc); + + // No matches over the non-link + { + var match = try set.matchSet( + alloc, + &t.screen, + .{ .x = 2, .y = 0 }, + inputpkg.ctrlOrSuper(.{}), + ); + defer match.deinit(alloc); + try testing.expectEqual(@as(usize, 0), match.matches.len); + } + + // Match over link + var match = try set.matchSet( + alloc, + &t.screen, + .{ .x = 3, .y = 0 }, + inputpkg.ctrlOrSuper(.{}), + ); + defer match.deinit(alloc); + try testing.expectEqual(@as(usize, 1), match.matches.len); + + // Test our matches + try testing.expect(!match.orderedContains(s, s.pages.pin(.{ .screen = .{ + .x = 2, + .y = 0, + } }).?)); + try testing.expect(match.orderedContains(s, s.pages.pin(.{ .screen = .{ + .x = 3, + .y = 0, + } }).?)); + try testing.expect(match.orderedContains(s, s.pages.pin(.{ .screen = .{ + .x = 4, + .y = 0, + } }).?)); + try testing.expect(match.orderedContains(s, s.pages.pin(.{ .screen = .{ + .x = 5, + .y = 0, + } }).?)); + try testing.expect(!match.orderedContains(s, s.pages.pin(.{ .screen = .{ + .x = 6, + .y = 0, + } }).?)); +} diff --git a/src/terminal/page.zig b/src/terminal/page.zig index b8fe8e9d0..e807904df 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -916,7 +916,7 @@ pub const Page = struct { } /// Returns the hyperlink ID for the given cell. - pub fn lookupHyperlink(self: *const Page, cell: *Cell) ?hyperlink.Id { + pub fn lookupHyperlink(self: *const Page, cell: *const Cell) ?hyperlink.Id { const cell_offset = getOffset(Cell, self.memory, cell); const map = self.hyperlink_map.map(self.memory); return map.get(cell_offset);