mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
terminal: move line searching here, unit test
This commit is contained in:
110
src/Surface.zig
110
src/Surface.zig
@ -20,6 +20,7 @@ const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const oni = @import("oniguruma");
|
||||
const ziglyph = @import("ziglyph");
|
||||
const main = @import("main.zig");
|
||||
const renderer = @import("renderer.zig");
|
||||
@ -165,12 +166,38 @@ const DerivedConfig = struct {
|
||||
window_padding_y: u32,
|
||||
window_padding_balance: bool,
|
||||
title: ?[:0]const u8,
|
||||
links: []const Link,
|
||||
|
||||
const Link = struct {
|
||||
regex: oni.Regex,
|
||||
action: input.Link.Action,
|
||||
};
|
||||
|
||||
pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
|
||||
var arena = ArenaAllocator.init(alloc_gpa);
|
||||
errdefer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// Build all of our links
|
||||
const links = links: {
|
||||
var links = std.ArrayList(Link).init(alloc);
|
||||
defer links.deinit();
|
||||
for (config.link.links.items) |link| {
|
||||
var regex = try link.oniRegex();
|
||||
errdefer regex.deinit();
|
||||
try links.append(.{
|
||||
.regex = regex,
|
||||
.action = link.action,
|
||||
});
|
||||
}
|
||||
|
||||
break :links try links.toOwnedSlice();
|
||||
};
|
||||
errdefer {
|
||||
for (links) |*link| link.regex.deinit();
|
||||
alloc.free(links);
|
||||
}
|
||||
|
||||
return .{
|
||||
.original_font_size = config.@"font-size",
|
||||
.keybind = try config.keybind.clone(alloc),
|
||||
@ -192,6 +219,7 @@ const DerivedConfig = struct {
|
||||
.window_padding_y = config.@"window-padding-y",
|
||||
.window_padding_balance = config.@"window-padding-balance",
|
||||
.title = config.title,
|
||||
.links = links,
|
||||
|
||||
// Assignments happen sequentially so we have to do this last
|
||||
// so that the memory is captured from allocs above.
|
||||
@ -1842,6 +1870,14 @@ pub fn mouseButtonCallback(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle link clicking. We want to do this before we do mouse
|
||||
// reporting or any other mouse handling because a successfully
|
||||
// clicked link will swallow the event.
|
||||
if (button == .left and action == .release) {
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
if (try self.processLinks(pos)) return;
|
||||
}
|
||||
|
||||
// Report mouse events if enabled
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
@ -1970,6 +2006,28 @@ pub fn mouseButtonCallback(
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to invoke the action of any link that is under the
|
||||
/// given position.
|
||||
fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
|
||||
// If we have no configured links we can save a lot of work
|
||||
if (self.config.links.len == 0) return false;
|
||||
|
||||
// Convert our cursor position to a screen point.
|
||||
const screen_point = screen_point: {
|
||||
const viewport_point = self.posToViewport(pos.x, pos.y);
|
||||
break :screen_point viewport_point.toScreen(&self.io.terminal.screen);
|
||||
};
|
||||
|
||||
// Get the line we're hovering over.
|
||||
const line = self.io.terminal.screen.getLine(screen_point) orelse
|
||||
return false;
|
||||
const strmap = try line.stringMap(self.alloc);
|
||||
defer strmap.deinit(self.alloc);
|
||||
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn cursorPosCallback(
|
||||
self: *Surface,
|
||||
pos: apprt.CursorPos,
|
||||
@ -2016,36 +2074,38 @@ pub fn cursorPosCallback(
|
||||
return;
|
||||
}
|
||||
|
||||
// If the cursor isn't clicked currently, it doesn't matter
|
||||
if (self.mouse.click_state[@intFromEnum(input.MouseButton.left)] != .press) return;
|
||||
// Handle cursor position for text selection
|
||||
if (self.mouse.click_state[@intFromEnum(input.MouseButton.left)] == .press) {
|
||||
// All roads lead to requiring a re-render at this point.
|
||||
try self.queueRender();
|
||||
|
||||
// All roads lead to requiring a re-render at this point.
|
||||
try self.queueRender();
|
||||
// If our y is negative, we're above the window. In this case, we scroll
|
||||
// up. The amount we scroll up is dependent on how negative we are.
|
||||
// Note: one day, we can change this from distance to time based if we want.
|
||||
//log.warn("CURSOR POS: {} {}", .{ pos, self.screen_size });
|
||||
const max_y: f32 = @floatFromInt(self.screen_size.height);
|
||||
if (pos.y < 0 or pos.y > max_y) {
|
||||
const delta: isize = if (pos.y < 0) -1 else 1;
|
||||
try self.io.terminal.scrollViewport(.{ .delta = delta });
|
||||
|
||||
// If our y is negative, we're above the window. In this case, we scroll
|
||||
// up. The amount we scroll up is dependent on how negative we are.
|
||||
// Note: one day, we can change this from distance to time based if we want.
|
||||
//log.warn("CURSOR POS: {} {}", .{ pos, self.screen_size });
|
||||
const max_y: f32 = @floatFromInt(self.screen_size.height);
|
||||
if (pos.y < 0 or pos.y > max_y) {
|
||||
const delta: isize = if (pos.y < 0) -1 else 1;
|
||||
try self.io.terminal.scrollViewport(.{ .delta = delta });
|
||||
// TODO: We want a timer or something to repeat while we're still
|
||||
// at this cursor position. Right now, the user has to jiggle their
|
||||
// mouse in order to scroll.
|
||||
}
|
||||
|
||||
// TODO: We want a timer or something to repeat while we're still
|
||||
// at this cursor position. Right now, the user has to jiggle their
|
||||
// mouse in order to scroll.
|
||||
}
|
||||
// Convert to points
|
||||
const viewport_point = self.posToViewport(pos.x, pos.y);
|
||||
const screen_point = viewport_point.toScreen(&self.io.terminal.screen);
|
||||
|
||||
// Convert to points
|
||||
const viewport_point = self.posToViewport(pos.x, pos.y);
|
||||
const screen_point = viewport_point.toScreen(&self.io.terminal.screen);
|
||||
// Handle dragging depending on click count
|
||||
switch (self.mouse.left_click_count) {
|
||||
1 => self.dragLeftClickSingle(screen_point, pos.x),
|
||||
2 => self.dragLeftClickDouble(screen_point),
|
||||
3 => self.dragLeftClickTriple(screen_point),
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
// Handle dragging depending on click count
|
||||
switch (self.mouse.left_click_count) {
|
||||
1 => self.dragLeftClickSingle(screen_point, pos.x),
|
||||
2 => self.dragLeftClickDouble(screen_point),
|
||||
3 => self.dragLeftClickTriple(screen_point),
|
||||
else => unreachable,
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1408,29 +1408,17 @@ fn rebuildCells(
|
||||
const strmap = line.stringMap(arena_alloc) catch continue;
|
||||
defer strmap.deinit(arena_alloc);
|
||||
|
||||
var offset: usize = 0;
|
||||
var it = strmap.searchIterator(self.url_regex);
|
||||
while (true) {
|
||||
var match = self.url_regex.search(strmap.string[offset..], .{}) catch |err| {
|
||||
switch (err) {
|
||||
error.Mismatch => {},
|
||||
else => log.warn("failed to search for URLs err={}", .{err}),
|
||||
}
|
||||
|
||||
const match_ = it.next() catch |err| {
|
||||
log.warn("failed to search for URLs err={}", .{err});
|
||||
break;
|
||||
};
|
||||
var match = match_ orelse break;
|
||||
defer match.deinit();
|
||||
|
||||
// Determine the screen point for the match
|
||||
const start_idx: usize = @intCast(match.starts()[0]);
|
||||
const end_idx: usize = @intCast(match.ends()[0] - 1);
|
||||
const start_pt = strmap.map[offset + start_idx];
|
||||
const end_pt = strmap.map[offset + end_idx];
|
||||
|
||||
// Move our offset so we can continue searching
|
||||
offset += end_idx;
|
||||
|
||||
// Store our selection
|
||||
try urls.append(.{ .start = start_pt, .end = end_pt });
|
||||
try urls.append(match.selection());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,7 @@ const kitty = @import("kitty.zig");
|
||||
const point = @import("point.zig");
|
||||
const CircBuf = @import("../circ_buf.zig").CircBuf;
|
||||
const Selection = @import("Selection.zig");
|
||||
const StringMap = @import("StringMap.zig");
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
const charsets = @import("charsets.zig");
|
||||
|
||||
@ -2228,18 +2229,6 @@ pub fn selectionString(
|
||||
return string;
|
||||
}
|
||||
|
||||
/// A string along with the mapping of each individual byte in the string
|
||||
/// to the point in the screen.
|
||||
pub const StringMap = struct {
|
||||
string: [:0]const u8,
|
||||
map: []point.ScreenPoint,
|
||||
|
||||
pub fn deinit(self: StringMap, alloc: Allocator) void {
|
||||
alloc.free(self.string);
|
||||
alloc.free(self.map);
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the row text associated with a selection along with the
|
||||
/// mapping of each individual byte in the string to the point in the screen.
|
||||
fn selectionStringMap(
|
||||
|
124
src/terminal/StringMap.zig
Normal file
124
src/terminal/StringMap.zig
Normal file
@ -0,0 +1,124 @@
|
||||
/// A string along with the mapping of each individual byte in the string
|
||||
/// to the point in the screen.
|
||||
const StringMap = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const oni = @import("oniguruma");
|
||||
const point = @import("point.zig");
|
||||
const Selection = @import("Selection.zig");
|
||||
const Screen = @import("Screen.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
string: [:0]const u8,
|
||||
map: []point.ScreenPoint,
|
||||
|
||||
pub fn deinit(self: StringMap, alloc: Allocator) void {
|
||||
alloc.free(self.string);
|
||||
alloc.free(self.map);
|
||||
}
|
||||
|
||||
/// Returns an iterator that yields the next match of the given regex.
|
||||
pub fn searchIterator(
|
||||
self: StringMap,
|
||||
regex: oni.Regex,
|
||||
) SearchIterator {
|
||||
return .{ .map = self, .regex = regex };
|
||||
}
|
||||
|
||||
/// Iterates over the regular expression matches of the string.
|
||||
pub const SearchIterator = struct {
|
||||
map: StringMap,
|
||||
regex: oni.Regex,
|
||||
offset: usize = 0,
|
||||
|
||||
/// Returns the next regular expression match or null if there are
|
||||
/// no more matches.
|
||||
pub fn next(self: *SearchIterator) !?Match {
|
||||
if (self.offset >= self.map.string.len) return null;
|
||||
|
||||
var region = self.regex.search(
|
||||
self.map.string[self.offset..],
|
||||
.{},
|
||||
) catch |err| switch (err) {
|
||||
error.Mismatch => {
|
||||
self.offset = self.map.string.len;
|
||||
return null;
|
||||
},
|
||||
|
||||
else => return err,
|
||||
};
|
||||
errdefer region.deinit();
|
||||
|
||||
// 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.
|
||||
const end_idx: usize = @intCast(region.ends()[0]);
|
||||
defer self.offset += end_idx;
|
||||
|
||||
return .{
|
||||
.map = self.map,
|
||||
.offset = self.offset,
|
||||
.region = region,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// A single regular expression match.
|
||||
pub const Match = struct {
|
||||
map: StringMap,
|
||||
offset: usize,
|
||||
region: oni.Region,
|
||||
|
||||
pub fn deinit(self: *Match) void {
|
||||
self.region.deinit();
|
||||
}
|
||||
|
||||
/// Returns the selection containing the full match.
|
||||
pub fn selection(self: Match) Selection {
|
||||
const start_idx: usize = @intCast(self.region.starts()[0]);
|
||||
const end_idx: usize = @intCast(self.region.ends()[0] - 1);
|
||||
const start_pt = self.map.map[self.offset + start_idx];
|
||||
const end_pt = self.map.map[self.offset + end_idx];
|
||||
return .{ .start = start_pt, .end = end_pt };
|
||||
}
|
||||
};
|
||||
|
||||
test "searchIterator" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Initialize our regex
|
||||
try oni.testing.ensureInit();
|
||||
var re = try oni.Regex.init(
|
||||
"[A-B]{2}",
|
||||
.{},
|
||||
oni.Encoding.utf8,
|
||||
oni.Syntax.default,
|
||||
null,
|
||||
);
|
||||
defer re.deinit();
|
||||
|
||||
// Initialize our screen
|
||||
var s = try Screen.init(alloc, 5, 5, 0);
|
||||
defer s.deinit();
|
||||
const str = "1ABCD2EFGH\n3IJKL";
|
||||
try s.testWriteString(str);
|
||||
const line = s.getLine(.{ .x = 2, .y = 1 }).?;
|
||||
const map = try line.stringMap(alloc);
|
||||
defer map.deinit(alloc);
|
||||
|
||||
// Get our iterator
|
||||
var it = map.searchIterator(re);
|
||||
{
|
||||
var match = (try it.next()).?;
|
||||
defer match.deinit();
|
||||
|
||||
const sel = match.selection();
|
||||
try testing.expectEqual(Selection{
|
||||
.start = .{ .x = 1, .y = 0 },
|
||||
.end = .{ .x = 2, .y = 0 },
|
||||
}, sel);
|
||||
}
|
||||
|
||||
try testing.expect(try it.next() == null);
|
||||
}
|
@ -26,6 +26,7 @@ pub const Terminal = @import("Terminal.zig");
|
||||
pub const Parser = @import("Parser.zig");
|
||||
pub const Selection = @import("Selection.zig");
|
||||
pub const Screen = @import("Screen.zig");
|
||||
pub const StringMap = @import("StringMap.zig");
|
||||
pub const Stream = stream.Stream;
|
||||
pub const Cursor = Screen.Cursor;
|
||||
pub const CursorStyleReq = ansi.CursorStyle;
|
||||
|
Reference in New Issue
Block a user