terminal: move line searching here, unit test

This commit is contained in:
Mitchell Hashimoto
2023-11-28 13:55:57 -08:00
parent be05c3af53
commit aa86031ff6
5 changed files with 216 additions and 54 deletions

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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
View 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);
}

View File

@ -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;