mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
terminal: add StringMap back
This commit is contained in:
@ -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.
|
||||
|
@ -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
140
src/terminal/StringMap.zig
Normal 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);
|
||||
}
|
@ -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;
|
||||
|
Reference in New Issue
Block a user