From 41dd733ca7123c9ac4bfc6c781697a660e95b052 Mon Sep 17 00:00:00 2001 From: Can Celik Date: Sat, 4 Apr 2026 19:22:04 +0300 Subject: feat: finish ghostty pane backend integration --- vendor/libghostty-vt/src/terminal/StringMap.zig | 298 ++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 vendor/libghostty-vt/src/terminal/StringMap.zig (limited to 'vendor/libghostty-vt/src/terminal/StringMap.zig') diff --git a/vendor/libghostty-vt/src/terminal/StringMap.zig b/vendor/libghostty-vt/src/terminal/StringMap.zig new file mode 100644 index 00000000..18dd7b19 --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/StringMap.zig @@ -0,0 +1,298 @@ +/// 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 build_options = @import("terminal_options"); +const oni = @import("oniguruma"); +const point = @import("point.zig"); +const Selection = @import("Selection.zig"); +const Screen = @import("Screen.zig"); +const Pin = @import("PageList.zig").Pin; +const Allocator = std.mem.Allocator; + +// Retry budget for StringMap regex searches. +// +// Units are Oniguruma retry steps (internal backtracking/retry counter), +// not bytes/characters/time. +const oni_search_retry_limit = 100_000; + +string: [:0]const u8, +map: []Pin, + +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. +/// Requires Ghostty to be compiled with regex support. +pub const searchIterator = if (build_options.oniguruma) + searchIteratorOni +else + void; + +fn searchIteratorOni( + 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; + + // Use per-search match params so we can bound regex retry steps + // (Oniguruma's internal backtracking work counter). + var match_param = try oni.MatchParam.init(); + defer match_param.deinit(); + try match_param.setRetryLimitInSearch(oni_search_retry_limit); + + var region = self.regex.searchWithParam( + self.map.string[self.offset..], + .{}, + &match_param, + ) catch |err| switch (err) { + // Retry/stack-limit errors mean we hit our work budget and + // aborted matching. + // For iterator callers this is equivalent to "no further matches". + error.Mismatch, + error.RetryLimitInMatchOver, + error.RetryLimitInSearchOver, + error.MatchStackLimitOver, + error.SubexpCallLimitInSearchOver, + => { + 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 .init(start_pt, end_pt, false); + } +}; + +test "StringMap searchIterator" { + if (comptime !build_options.oniguruma) return error.SkipZigTest; + + 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, .{ .cols = 5, .rows = 5, .max_scrollback = 0 }); + defer s.deinit(); + const str = "1ABCD2EFGH\n3IJKL"; + try s.testWriteString(str); + const line = s.selectLine(.{ + .pin = s.pages.pin(.{ .active = .{ + .x = 2, + .y = 1, + } }).?, + }).?; + var map: StringMap = undefined; + const sel_str = try s.selectionString(alloc, .{ + .sel = line, + .trim = false, + .map = &map, + }); + alloc.free(sel_str); + 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(point.Point{ .screen = .{ + .x = 1, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.start()).?); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 2, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.end()).?); + } + + try testing.expect(try it.next() == null); +} + +test "StringMap searchIterator URL detection" { + if (comptime !build_options.oniguruma) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + const url = @import("../config/url.zig"); + + // Initialize URL regex + try oni.testing.ensureInit(); + var re = try oni.Regex.init( + url.regex, + .{}, + oni.Encoding.utf8, + oni.Syntax.default, + null, + ); + defer re.deinit(); + + // Initialize our screen with text containing a URL + var s = try Screen.init(alloc, .{ .cols = 40, .rows = 5, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello https://example.com/path world"); + + // Get the line + const line = s.selectLine(.{ + .pin = s.pages.pin(.{ .active = .{ + .x = 10, + .y = 0, + } }).?, + }).?; + var map: StringMap = undefined; + const sel_str = try s.selectionString(alloc, .{ + .sel = line, + .trim = false, + .map = &map, + }); + alloc.free(sel_str); + defer map.deinit(alloc); + + // Search for URL match + var it = map.searchIterator(re); + { + var match = (try it.next()).?; + defer match.deinit(); + + const sel = match.selection(); + // URL should start at x=6 ("https://example.com/path" starts after "hello ") + try testing.expectEqual(point.Point{ .screen = .{ + .x = 6, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.start()).?); + // URL should end at x=29 (end of "/path") + try testing.expectEqual(point.Point{ .screen = .{ + .x = 29, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.end()).?); + } + + try testing.expect(try it.next() == null); +} + +test "StringMap searchIterator URL with click position" { + if (comptime !build_options.oniguruma) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + const url = @import("../config/url.zig"); + + // Initialize URL regex + try oni.testing.ensureInit(); + var re = try oni.Regex.init( + url.regex, + .{}, + oni.Encoding.utf8, + oni.Syntax.default, + null, + ); + defer re.deinit(); + + // Initialize our screen with text containing a URL + var s = try Screen.init(alloc, .{ .cols = 40, .rows = 5, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello https://example.com world"); + + // Simulate clicking on "example" (x=14) + const click_pin = s.pages.pin(.{ .active = .{ + .x = 14, + .y = 0, + } }).?; + + // Get the line + const line = s.selectLine(.{ + .pin = click_pin, + }).?; + var map: StringMap = undefined; + const sel_str = try s.selectionString(alloc, .{ + .sel = line, + .trim = false, + .map = &map, + }); + alloc.free(sel_str); + defer map.deinit(alloc); + + // Search for URL match and verify click position is within URL + var it = map.searchIterator(re); + var found_url = false; + while (true) { + var match = (try it.next()) orelse break; + defer match.deinit(); + + const sel = match.selection(); + if (sel.contains(&s, click_pin)) { + found_url = true; + // Verify URL bounds + try testing.expectEqual(point.Point{ .screen = .{ + .x = 6, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.start()).?); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 24, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.end()).?); + break; + } + } + try testing.expect(found_url); +} -- cgit v1.2.3