mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
terminal: select whole links if regex matches
Added selectLink in src/terminal/Screen.zig Updated right click logic in src/Surface.zig
This commit is contained in:
@ -2735,10 +2735,17 @@ pub fn mouseButtonCallback(
|
||||
// word selection where we clicked.
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
Reference in New Issue
Block a user