mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +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 assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
const oni = @import("oniguruma");
|
||||||
const ziglyph = @import("ziglyph");
|
const ziglyph = @import("ziglyph");
|
||||||
const main = @import("main.zig");
|
const main = @import("main.zig");
|
||||||
const renderer = @import("renderer.zig");
|
const renderer = @import("renderer.zig");
|
||||||
@ -165,12 +166,38 @@ const DerivedConfig = struct {
|
|||||||
window_padding_y: u32,
|
window_padding_y: u32,
|
||||||
window_padding_balance: bool,
|
window_padding_balance: bool,
|
||||||
title: ?[:0]const u8,
|
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 {
|
pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
|
||||||
var arena = ArenaAllocator.init(alloc_gpa);
|
var arena = ArenaAllocator.init(alloc_gpa);
|
||||||
errdefer arena.deinit();
|
errdefer arena.deinit();
|
||||||
const alloc = arena.allocator();
|
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 .{
|
return .{
|
||||||
.original_font_size = config.@"font-size",
|
.original_font_size = config.@"font-size",
|
||||||
.keybind = try config.keybind.clone(alloc),
|
.keybind = try config.keybind.clone(alloc),
|
||||||
@ -192,6 +219,7 @@ const DerivedConfig = struct {
|
|||||||
.window_padding_y = config.@"window-padding-y",
|
.window_padding_y = config.@"window-padding-y",
|
||||||
.window_padding_balance = config.@"window-padding-balance",
|
.window_padding_balance = config.@"window-padding-balance",
|
||||||
.title = config.title,
|
.title = config.title,
|
||||||
|
.links = links,
|
||||||
|
|
||||||
// Assignments happen sequentially so we have to do this last
|
// Assignments happen sequentially so we have to do this last
|
||||||
// so that the memory is captured from allocs above.
|
// 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
|
// Report mouse events if enabled
|
||||||
{
|
{
|
||||||
self.renderer_state.mutex.lock();
|
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(
|
pub fn cursorPosCallback(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
pos: apprt.CursorPos,
|
pos: apprt.CursorPos,
|
||||||
@ -2016,36 +2074,38 @@ pub fn cursorPosCallback(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the cursor isn't clicked currently, it doesn't matter
|
// Handle cursor position for text selection
|
||||||
if (self.mouse.click_state[@intFromEnum(input.MouseButton.left)] != .press) return;
|
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.
|
// If our y is negative, we're above the window. In this case, we scroll
|
||||||
try self.queueRender();
|
// 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
|
// TODO: We want a timer or something to repeat while we're still
|
||||||
// up. The amount we scroll up is dependent on how negative we are.
|
// at this cursor position. Right now, the user has to jiggle their
|
||||||
// Note: one day, we can change this from distance to time based if we want.
|
// mouse in order to scroll.
|
||||||
//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
|
// Convert to points
|
||||||
// at this cursor position. Right now, the user has to jiggle their
|
const viewport_point = self.posToViewport(pos.x, pos.y);
|
||||||
// mouse in order to scroll.
|
const screen_point = viewport_point.toScreen(&self.io.terminal.screen);
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to points
|
// Handle dragging depending on click count
|
||||||
const viewport_point = self.posToViewport(pos.x, pos.y);
|
switch (self.mouse.left_click_count) {
|
||||||
const screen_point = viewport_point.toScreen(&self.io.terminal.screen);
|
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
|
return;
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1408,29 +1408,17 @@ fn rebuildCells(
|
|||||||
const strmap = line.stringMap(arena_alloc) catch continue;
|
const strmap = line.stringMap(arena_alloc) catch continue;
|
||||||
defer strmap.deinit(arena_alloc);
|
defer strmap.deinit(arena_alloc);
|
||||||
|
|
||||||
var offset: usize = 0;
|
var it = strmap.searchIterator(self.url_regex);
|
||||||
while (true) {
|
while (true) {
|
||||||
var match = self.url_regex.search(strmap.string[offset..], .{}) catch |err| {
|
const match_ = it.next() catch |err| {
|
||||||
switch (err) {
|
log.warn("failed to search for URLs err={}", .{err});
|
||||||
error.Mismatch => {},
|
|
||||||
else => log.warn("failed to search for URLs err={}", .{err}),
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
var match = match_ orelse break;
|
||||||
defer match.deinit();
|
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
|
// 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 point = @import("point.zig");
|
||||||
const CircBuf = @import("../circ_buf.zig").CircBuf;
|
const CircBuf = @import("../circ_buf.zig").CircBuf;
|
||||||
const Selection = @import("Selection.zig");
|
const Selection = @import("Selection.zig");
|
||||||
|
const StringMap = @import("StringMap.zig");
|
||||||
const fastmem = @import("../fastmem.zig");
|
const fastmem = @import("../fastmem.zig");
|
||||||
const charsets = @import("charsets.zig");
|
const charsets = @import("charsets.zig");
|
||||||
|
|
||||||
@ -2228,18 +2229,6 @@ pub fn selectionString(
|
|||||||
return string;
|
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
|
/// 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.
|
/// mapping of each individual byte in the string to the point in the screen.
|
||||||
fn selectionStringMap(
|
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 Parser = @import("Parser.zig");
|
||||||
pub const Selection = @import("Selection.zig");
|
pub const Selection = @import("Selection.zig");
|
||||||
pub const Screen = @import("Screen.zig");
|
pub const Screen = @import("Screen.zig");
|
||||||
|
pub const StringMap = @import("StringMap.zig");
|
||||||
pub const Stream = stream.Stream;
|
pub const Stream = stream.Stream;
|
||||||
pub const Cursor = Screen.Cursor;
|
pub const Cursor = Screen.Cursor;
|
||||||
pub const CursorStyleReq = ansi.CursorStyle;
|
pub const CursorStyleReq = ansi.CursorStyle;
|
||||||
|
Reference in New Issue
Block a user