diff --git a/src/terminal-old/main.zig b/src/terminal-old/main.zig index 5f29ce70c..0e6856a10 100644 --- a/src/terminal-old/main.zig +++ b/src/terminal-old/main.zig @@ -42,7 +42,6 @@ pub const EraseLine = csi.EraseLine; pub const TabClear = csi.TabClear; pub const Attribute = sgr.Attribute; -// TODO(paged-terminal) pub const StringMap = @import("StringMap.zig"); /// If we're targeting wasm then we export some wasm APIs. diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index d351de6ec..d22d185b2 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -10,6 +10,7 @@ const sgr = @import("sgr.zig"); const unicode = @import("../unicode/main.zig"); const Selection = @import("Selection.zig"); const PageList = @import("PageList.zig"); +const StringMap = @import("StringMap.zig"); const pagepkg = @import("page.zig"); const point = @import("point.zig"); const size = @import("size.zig"); @@ -1111,7 +1112,12 @@ pub const SelectionString = struct { sel: Selection, /// If true, trim whitespace around the selection. - trim: bool, + trim: bool = true, + + /// If non-null, a stringmap will be written here. This will use + /// the same allocator as the call to selectionString. The string will + /// be duplicated here and in the return value so both must be freed. + map: ?*StringMap = null, }; /// Returns the raw text associated with a selection. This will unwrap @@ -1124,6 +1130,11 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! var strbuilder = std.ArrayList(u8).init(alloc); defer strbuilder.deinit(); + // If we're building a stringmap, create our builder for the pins. + const MapBuilder = std.ArrayList(Pin); + var mapbuilder: ?MapBuilder = if (opts.map != null) MapBuilder.init(alloc) else null; + defer if (mapbuilder) |*b| b.deinit(); + const sel_ordered = opts.sel.ordered(self, .forward); const sel_start = start: { var start = sel_ordered.start(); @@ -1153,7 +1164,7 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! var row_count: usize = 0; while (page_it.next()) |chunk| { const rows = chunk.rows(); - for (rows) |row| { + for (rows, chunk.start..) |row, y| { const cells_ptr = row.cells.ptr(chunk.page.data.memory); const start_x = if (row_count == 0 or sel_ordered.rectangle) @@ -1166,7 +1177,7 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! self.pages.cols; const cells = cells_ptr[start_x..end_x]; - for (cells) |*cell| { + for (cells, start_x..) |*cell, x| { // Skip wide spacers switch (cell.wide) { .narrow, .wide => {}, @@ -1179,12 +1190,26 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! const char = if (raw > 0) raw else ' '; const encode_len = try std.unicode.utf8Encode(char, &buf); try strbuilder.appendSlice(buf[0..encode_len]); + if (mapbuilder) |*b| { + for (0..encode_len) |_| try b.append(.{ + .page = chunk.page, + .y = y, + .x = x, + }); + } } if (cell.hasGrapheme()) { const cps = chunk.page.data.lookupGrapheme(cell).?; for (cps) |cp| { const encode_len = try std.unicode.utf8Encode(cp, &buf); try strbuilder.appendSlice(buf[0..encode_len]); + if (mapbuilder) |*b| { + for (0..encode_len) |_| try b.append(.{ + .page = chunk.page, + .y = y, + .x = x, + }); + } } } } @@ -1193,12 +1218,32 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! (!row.wrap or sel_ordered.rectangle)) { try strbuilder.append('\n'); + if (mapbuilder) |*b| try b.append(.{ + .page = chunk.page, + .y = y, + .x = chunk.page.data.size.cols - 1, + }); } row_count += 1; } } + if (comptime std.debug.runtime_safety) { + if (mapbuilder) |b| assert(strbuilder.items.len == b.items.len); + } + + // If we have a mapbuilder, we need to setup our string map. + if (mapbuilder) |*b| { + var strclone = try strbuilder.clone(); + defer strclone.deinit(); + const str = try strclone.toOwnedSliceSentinel(0); + errdefer alloc.free(str); + const map = try b.toOwnedSlice(); + errdefer alloc.free(map); + opts.map.?.* = .{ .string = str, .map = map }; + } + // Remove any trailing spaces on lines. We could do optimize this by // doing this in the loop above but this isn't very hot path code and // this is simple. @@ -1267,7 +1312,7 @@ pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection { // The real start of the row is the first row in the soft-wrap. const start_pin: Pin = start_pin: { var it = opts.pin.rowIterator(.left_up, null); - var it_prev: Pin = opts.pin; + var it_prev: Pin = it.next().?; // skip self while (it.next()) |p| { const row = p.rowAndCell().row; @@ -5026,6 +5071,31 @@ test "Screen: selectLine across soft-wrap" { } } +test "Screen: selectLine across full soft-wrap" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 5, 5, 0); + defer s.deinit(); + try s.testWriteString("1ABCD2EFGH\n3IJKL"); + + { + var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ + .x = 2, + .y = 1, + } }).? }).?; + defer sel.deinit(&s); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.start()).?); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 4, + .y = 1, + } }, s.pages.pointFromPin(.screen, sel.end()).?); + } +} + test "Screen: selectLine across soft-wrap ignores blank lines" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/StringMap.zig b/src/terminal/StringMap.zig new file mode 100644 index 000000000..9892c13df --- /dev/null +++ b/src/terminal/StringMap.zig @@ -0,0 +1,140 @@ +/// 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 Pin = @import("PageList.zig").Pin; +const Allocator = std.mem.Allocator; + +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. +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 Selection.init(start_pt, end_pt, false); + } +}; + +test "StringMap 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.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); +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 05ebf069f..cf05771ec 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -35,7 +35,7 @@ pub const Pin = PageList.Pin; pub const Screen = @import("Screen.zig"); pub const ScreenType = Terminal.ScreenType; pub const Selection = @import("Selection.zig"); -//pub const StringMap = @import("StringMap.zig"); +pub const StringMap = @import("StringMap.zig"); pub const Style = style.Style; pub const Terminal = @import("Terminal.zig"); pub const Stream = stream.Stream;