terminal: add StringMap back

This commit is contained in:
Mitchell Hashimoto
2024-03-15 20:39:21 -07:00
parent bca51ee771
commit d664840b7f
4 changed files with 215 additions and 6 deletions

View File

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

View File

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

140
src/terminal/StringMap.zig Normal file
View File

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

View File

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