aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/StringMap.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/StringMap.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/StringMap.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/StringMap.zig298
1 files changed, 298 insertions, 0 deletions
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);
+}