mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +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 TabClear = csi.TabClear;
|
||||||
pub const Attribute = sgr.Attribute;
|
pub const Attribute = sgr.Attribute;
|
||||||
|
|
||||||
// TODO(paged-terminal)
|
|
||||||
pub const StringMap = @import("StringMap.zig");
|
pub const StringMap = @import("StringMap.zig");
|
||||||
|
|
||||||
/// If we're targeting wasm then we export some wasm APIs.
|
/// 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 unicode = @import("../unicode/main.zig");
|
||||||
const Selection = @import("Selection.zig");
|
const Selection = @import("Selection.zig");
|
||||||
const PageList = @import("PageList.zig");
|
const PageList = @import("PageList.zig");
|
||||||
|
const StringMap = @import("StringMap.zig");
|
||||||
const pagepkg = @import("page.zig");
|
const pagepkg = @import("page.zig");
|
||||||
const point = @import("point.zig");
|
const point = @import("point.zig");
|
||||||
const size = @import("size.zig");
|
const size = @import("size.zig");
|
||||||
@ -1111,7 +1112,12 @@ pub const SelectionString = struct {
|
|||||||
sel: Selection,
|
sel: Selection,
|
||||||
|
|
||||||
/// If true, trim whitespace around the 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
|
/// 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);
|
var strbuilder = std.ArrayList(u8).init(alloc);
|
||||||
defer strbuilder.deinit();
|
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_ordered = opts.sel.ordered(self, .forward);
|
||||||
const sel_start = start: {
|
const sel_start = start: {
|
||||||
var start = sel_ordered.start();
|
var start = sel_ordered.start();
|
||||||
@ -1153,7 +1164,7 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) !
|
|||||||
var row_count: usize = 0;
|
var row_count: usize = 0;
|
||||||
while (page_it.next()) |chunk| {
|
while (page_it.next()) |chunk| {
|
||||||
const rows = chunk.rows();
|
const rows = chunk.rows();
|
||||||
for (rows) |row| {
|
for (rows, chunk.start..) |row, y| {
|
||||||
const cells_ptr = row.cells.ptr(chunk.page.data.memory);
|
const cells_ptr = row.cells.ptr(chunk.page.data.memory);
|
||||||
|
|
||||||
const start_x = if (row_count == 0 or sel_ordered.rectangle)
|
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;
|
self.pages.cols;
|
||||||
|
|
||||||
const cells = cells_ptr[start_x..end_x];
|
const cells = cells_ptr[start_x..end_x];
|
||||||
for (cells) |*cell| {
|
for (cells, start_x..) |*cell, x| {
|
||||||
// Skip wide spacers
|
// Skip wide spacers
|
||||||
switch (cell.wide) {
|
switch (cell.wide) {
|
||||||
.narrow, .wide => {},
|
.narrow, .wide => {},
|
||||||
@ -1179,12 +1190,26 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) !
|
|||||||
const char = if (raw > 0) raw else ' ';
|
const char = if (raw > 0) raw else ' ';
|
||||||
const encode_len = try std.unicode.utf8Encode(char, &buf);
|
const encode_len = try std.unicode.utf8Encode(char, &buf);
|
||||||
try strbuilder.appendSlice(buf[0..encode_len]);
|
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()) {
|
if (cell.hasGrapheme()) {
|
||||||
const cps = chunk.page.data.lookupGrapheme(cell).?;
|
const cps = chunk.page.data.lookupGrapheme(cell).?;
|
||||||
for (cps) |cp| {
|
for (cps) |cp| {
|
||||||
const encode_len = try std.unicode.utf8Encode(cp, &buf);
|
const encode_len = try std.unicode.utf8Encode(cp, &buf);
|
||||||
try strbuilder.appendSlice(buf[0..encode_len]);
|
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))
|
(!row.wrap or sel_ordered.rectangle))
|
||||||
{
|
{
|
||||||
try strbuilder.append('\n');
|
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;
|
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
|
// 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
|
// doing this in the loop above but this isn't very hot path code and
|
||||||
// this is simple.
|
// 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.
|
// The real start of the row is the first row in the soft-wrap.
|
||||||
const start_pin: Pin = start_pin: {
|
const start_pin: Pin = start_pin: {
|
||||||
var it = opts.pin.rowIterator(.left_up, null);
|
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| {
|
while (it.next()) |p| {
|
||||||
const row = p.rowAndCell().row;
|
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" {
|
test "Screen: selectLine across soft-wrap ignores blank lines" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
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 Screen = @import("Screen.zig");
|
||||||
pub const ScreenType = Terminal.ScreenType;
|
pub const ScreenType = Terminal.ScreenType;
|
||||||
pub const Selection = @import("Selection.zig");
|
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 Style = style.Style;
|
||||||
pub const Terminal = @import("Terminal.zig");
|
pub const Terminal = @import("Terminal.zig");
|
||||||
pub const Stream = stream.Stream;
|
pub const Stream = stream.Stream;
|
||||||
|
Reference in New Issue
Block a user