From f4e67b3c50c126a2faee36a5e9245e43e12bbdbf Mon Sep 17 00:00:00 2001 From: RyzechDev // Ryz Date: Sun, 29 Sep 2024 10:54:33 -0500 Subject: [PATCH] terminal: select whole links if regex matches Added selectLink in src/terminal/Screen.zig Updated right click logic in src/Surface.zig --- src/Surface.zig | 13 +++-- src/terminal/Screen.zig | 112 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index e8bbb885f..e880f684e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2735,9 +2735,16 @@ pub fn mouseButtonCallback( // word selection where we clicked. } - const sel = screen.selectWord(pin) orelse break :sel; - try self.setSelection(sel); - try self.queueRender(); + // Check if the selection contains a link, if it does + // select the whole link, if it doesn't select a word. + if (screen.selectLink(pin) catch null) |sel| { + try self.setSelection(sel); + try self.queueRender(); + } else { + const sel = screen.selectWord(pin) orelse break :sel; + try self.setSelection(sel); + try self.queueRender(); + } } return false; diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 7927d0343..8161cb203 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -10,6 +10,8 @@ const fastmem = @import("../fastmem.zig"); const kitty = @import("kitty.zig"); const sgr = @import("sgr.zig"); const unicode = @import("../unicode/main.zig"); +const url = @import("../config/url.zig"); +const oni = @import("oniguruma"); const Selection = @import("Selection.zig"); const PageList = @import("PageList.zig"); const StringMap = @import("StringMap.zig"); @@ -2393,6 +2395,116 @@ pub fn selectWordBetween( return null; } +/// Returns a potentially null selection of a link +/// if it is null then there is no link match found. +pub fn selectLink(self: *Screen, pin: Pin) !?Selection { + // Boundary character, only need to check for whitespace + const boundary = &[_]u32{ + ' ', + '\t', + }; + + const start_cell = pin.rowAndCell().cell; + if (!start_cell.hasText()) return null; + + // Determine if we are a boundary or not to determine what our boundary is. + const expect_boundary = std.mem.indexOfAny( + u32, + boundary, + &[_]u32{start_cell.content.codepoint}, + ) != null; + + // Go forwards to find our end boundary + const end: Pin = end: { + var it = pin.cellIterator(.right_down, null); + var prev = it.next().?; // Consume one, our start + while (it.next()) |p| { + const rac = p.rowAndCell(); + const cell = rac.cell; + + // If we reached an empty cell its always a boundary + if (!cell.hasText()) break :end prev; + + // If we do not match our expected set, we hit a boundary + const this_boundary = std.mem.indexOfAny( + u32, + boundary, + &[_]u32{cell.content.codepoint}, + ) != null; + if (this_boundary != expect_boundary) break :end prev; + + // If we are going to the next row and it isn't wrapped, we + // return the previous. + if (p.x == p.page.data.size.cols - 1 and !rac.row.wrap) { + break :end p; + } + + prev = p; + } + + break :end prev; + }; + + // Go backwards to find our start boundary + const start: Pin = start: { + var it = pin.cellIterator(.left_up, null); + var prev = it.next().?; // Consume one, our start + while (it.next()) |p| { + const rac = p.rowAndCell(); + const cell = rac.cell; + + // If we are going to the next row and it isn't wrapped, we + // return the previous. + if (p.x == p.page.data.size.cols - 1 and !rac.row.wrap) { + break :start prev; + } + + // If we reached an empty cell its always a boundary + if (!cell.hasText()) break :start prev; + + // If we do not match our expected set, we hit a boundary + const this_boundary = std.mem.indexOfAny( + u32, + boundary, + &[_]u32{cell.content.codepoint}, + ) != null; + if (this_boundary != expect_boundary) break :start prev; + + prev = p; + } + + break :start prev; + }; + + // Get the selection and convert it into a usable format for regex matching. + const sel = Selection.init(start, end, false); + const raw_str = try selectionString(self, self.alloc, .{ + .sel = sel, + }); + defer self.alloc.free(raw_str); + + var re = try oni.Regex.init( + url.regex, + .{ .capture_group = true }, + oni.Encoding.utf8, + oni.Syntax.default, + null, + ); + defer re.deinit(); + + // Search the regex we just created for any matches + // If the count is greater than 0, meaning there's at least one match + // It will return the link selection + // Otherwise it returns null + var search = try re.search(raw_str, .{}); + defer search.deinit(); + if (search.count() > 0) { + return sel; + } + + return null; +} + /// Select the word under the given point. A word is any consecutive series /// of characters that are exclusively whitespace or exclusively non-whitespace. /// A selection can span multiple physical lines if they are soft-wrapped.