terminal: remove all legacy encodeUtf8 functions, replace with formatter (#9392)
This removes all existing functionality that I know of that encodes a terminal, screen, pagelist, or page as plaintext and unifies all logic onto the formatter system.pull/9403/head
parent
028ce83d46
commit
a4d54dca1c
|
|
@ -3216,50 +3216,6 @@ pub fn getCell(self: *const PageList, pt: point.Point) ?Cell {
|
|||
};
|
||||
}
|
||||
|
||||
pub const EncodeUtf8Options = struct {
|
||||
/// The start and end points of the dump, both inclusive. The x will
|
||||
/// be ignored and the full row will always be dumped.
|
||||
tl: Pin,
|
||||
br: ?Pin = null,
|
||||
|
||||
/// If true, this will unwrap soft-wrapped lines. If false, this will
|
||||
/// dump the screen as it is visually seen in a rendered window.
|
||||
unwrap: bool = true,
|
||||
|
||||
/// See Page.EncodeUtf8Options.
|
||||
cell_map: ?*Page.CellMap = null,
|
||||
};
|
||||
|
||||
/// Encode the pagelist to utf8 to the given writer.
|
||||
///
|
||||
/// The writer should be buffered; this function does not attempt to
|
||||
/// efficiently write and often writes one byte at a time.
|
||||
///
|
||||
/// Note: this is tested using Screen.dumpString. This is a function that
|
||||
/// predates this and is a thin wrapper around it so the tests all live there.
|
||||
pub fn encodeUtf8(
|
||||
self: *const PageList,
|
||||
writer: *std.Io.Writer,
|
||||
opts: EncodeUtf8Options,
|
||||
) anyerror!void {
|
||||
// We don't currently use self at all. There is an argument that this
|
||||
// function should live on Pin instead but there is some future we might
|
||||
// need state on here so... letting it go.
|
||||
_ = self;
|
||||
|
||||
var page_opts: Page.EncodeUtf8Options = .{
|
||||
.unwrap = opts.unwrap,
|
||||
.cell_map = opts.cell_map,
|
||||
};
|
||||
var iter = opts.tl.pageIterator(.right_down, opts.br);
|
||||
while (iter.next()) |chunk| {
|
||||
const page: *const Page = &chunk.node.data;
|
||||
page_opts.start_y = chunk.start;
|
||||
page_opts.end_y = chunk.end;
|
||||
page_opts.preceding = try page.encodeUtf8(writer, page_opts);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a debug diagram of the page list to the provided writer.
|
||||
///
|
||||
/// EXAMPLE:
|
||||
|
|
@ -3857,13 +3813,17 @@ pub fn getBottomRight(self: *const PageList, tag: point.Tag) ?Pin {
|
|||
},
|
||||
|
||||
.viewport => viewport: {
|
||||
const tl = self.getTopLeft(.viewport);
|
||||
break :viewport tl.down(self.rows - 1).?;
|
||||
var br = self.getTopLeft(.viewport);
|
||||
br = br.down(self.rows - 1).?;
|
||||
br.x = br.node.data.size.cols - 1;
|
||||
break :viewport br;
|
||||
},
|
||||
|
||||
.history => active: {
|
||||
const tl = self.getTopLeft(.active);
|
||||
break :active tl.up(1);
|
||||
var br = self.getTopLeft(.active);
|
||||
br = br.up(1) orelse return null;
|
||||
br.x = br.node.data.size.cols - 1;
|
||||
break :active br;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2799,9 +2799,38 @@ pub fn promptPath(
|
|||
pub fn dumpString(
|
||||
self: *const Screen,
|
||||
writer: *std.Io.Writer,
|
||||
opts: PageList.EncodeUtf8Options,
|
||||
) anyerror!void {
|
||||
try self.pages.encodeUtf8(writer, opts);
|
||||
opts: struct {
|
||||
/// The start and end points of the dump, both inclusive. The x will
|
||||
/// be ignored and the full row will always be dumped.
|
||||
tl: Pin,
|
||||
br: ?Pin = null,
|
||||
|
||||
/// If true, this will unwrap soft-wrapped lines. If false, this will
|
||||
/// dump the screen as it is visually seen in a rendered window.
|
||||
unwrap: bool = true,
|
||||
},
|
||||
) std.Io.Writer.Error!void {
|
||||
// Create a formatter and use that to emit our text.
|
||||
var formatter: ScreenFormatter = .init(self, .{
|
||||
.emit = .plain,
|
||||
.unwrap = opts.unwrap,
|
||||
.trim = false,
|
||||
});
|
||||
|
||||
// Set up the selection based on the pins
|
||||
const tl = opts.tl;
|
||||
const br = opts.br orelse self.pages.getBottomRight(.screen).?;
|
||||
|
||||
formatter.content = .{
|
||||
.selection = Selection.init(
|
||||
tl,
|
||||
br,
|
||||
false, // not rectangle
|
||||
),
|
||||
};
|
||||
|
||||
// Emit
|
||||
try formatter.format(writer);
|
||||
}
|
||||
|
||||
/// You should use dumpString, this is a restricted version mostly for
|
||||
|
|
@ -8916,81 +8945,3 @@ test "Screen: adjustCapacity cursor style exceeds style set capacity" {
|
|||
try testing.expect(s.cursor.style.default());
|
||||
try testing.expectEqual(style.default_id, s.cursor.style_id);
|
||||
}
|
||||
|
||||
test "Screen UTF8 cell map with newlines" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try Screen.init(alloc, 80, 24, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("A\n\nB\n\nC");
|
||||
|
||||
var cell_map = Page.CellMap.init(alloc);
|
||||
defer cell_map.deinit();
|
||||
var builder: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer builder.deinit();
|
||||
try s.dumpString(&builder.writer, .{
|
||||
.tl = s.pages.getTopLeft(.screen),
|
||||
.br = s.pages.getBottomRight(.screen),
|
||||
.cell_map = &cell_map,
|
||||
});
|
||||
|
||||
try testing.expectEqual(7, builder.written().len);
|
||||
try testing.expectEqualStrings("A\n\nB\n\nC", builder.written());
|
||||
try testing.expectEqual(builder.written().len, cell_map.map.items.len);
|
||||
try testing.expectEqual(Page.CellMapEntry{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
}, cell_map.map.items[0]);
|
||||
try testing.expectEqual(Page.CellMapEntry{
|
||||
.x = 1,
|
||||
.y = 0,
|
||||
}, cell_map.map.items[1]);
|
||||
try testing.expectEqual(Page.CellMapEntry{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
}, cell_map.map.items[2]);
|
||||
try testing.expectEqual(Page.CellMapEntry{
|
||||
.x = 0,
|
||||
.y = 2,
|
||||
}, cell_map.map.items[3]);
|
||||
}
|
||||
|
||||
test "Screen UTF8 cell map with blank prefix" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try Screen.init(alloc, 80, 24, 0);
|
||||
defer s.deinit();
|
||||
s.cursorAbsolute(2, 1);
|
||||
try s.testWriteString("B");
|
||||
|
||||
var cell_map: Page.CellMap = .init(alloc);
|
||||
defer cell_map.deinit();
|
||||
var builder: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer builder.deinit();
|
||||
try s.dumpString(&builder.writer, .{
|
||||
.tl = s.pages.getTopLeft(.screen),
|
||||
.br = s.pages.getBottomRight(.screen),
|
||||
.cell_map = &cell_map,
|
||||
});
|
||||
|
||||
try testing.expectEqualStrings("\n B", builder.written());
|
||||
try testing.expectEqual(builder.written().len, cell_map.map.items.len);
|
||||
try testing.expectEqual(Page.CellMapEntry{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
}, cell_map.map.items[0]);
|
||||
try testing.expectEqual(Page.CellMapEntry{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
}, cell_map.map.items[1]);
|
||||
try testing.expectEqual(Page.CellMapEntry{
|
||||
.x = 1,
|
||||
.y = 1,
|
||||
}, cell_map.map.items[2]);
|
||||
try testing.expectEqual(Page.CellMapEntry{
|
||||
.x = 2,
|
||||
.y = 1,
|
||||
}, cell_map.map.items[3]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1501,193 +1501,6 @@ pub const Page = struct {
|
|||
return self.grapheme_map.map(self.memory).capacity();
|
||||
}
|
||||
|
||||
/// Options for encoding the page as UTF-8.
|
||||
pub const EncodeUtf8Options = struct {
|
||||
/// The range of rows to encode. If end_y is null, then it will
|
||||
/// encode to the end of the page.
|
||||
start_y: size.CellCountInt = 0,
|
||||
end_y: ?size.CellCountInt = null,
|
||||
|
||||
/// If true, this will unwrap soft-wrapped lines. If false, this will
|
||||
/// dump the screen as it is visually seen in a rendered window.
|
||||
unwrap: bool = true,
|
||||
|
||||
/// Preceding state from encoding the prior page. Used to preserve
|
||||
/// blanks properly across multiple pages.
|
||||
preceding: TrailingUtf8State = .{},
|
||||
|
||||
/// If non-null, this will be cleared and filled with the x/y
|
||||
/// coordinates of each byte in the UTF-8 encoded output.
|
||||
/// The index in the array is the byte offset in the output
|
||||
/// where 0 is the cursor of the writer when the function is
|
||||
/// called.
|
||||
cell_map: ?*CellMap = null,
|
||||
|
||||
/// Trailing state for UTF-8 encoding.
|
||||
pub const TrailingUtf8State = struct {
|
||||
rows: usize = 0,
|
||||
cells: usize = 0,
|
||||
};
|
||||
};
|
||||
|
||||
/// See cell_map
|
||||
pub const CellMap = struct {
|
||||
alloc: Allocator,
|
||||
map: std.ArrayList(CellMapEntry),
|
||||
|
||||
pub fn init(alloc: Allocator) CellMap {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.map = .empty,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *CellMap) void {
|
||||
self.map.deinit(self.alloc);
|
||||
}
|
||||
};
|
||||
|
||||
/// The x/y coordinate of a single cell in the cell map.
|
||||
pub const CellMapEntry = struct {
|
||||
y: size.CellCountInt,
|
||||
x: size.CellCountInt,
|
||||
};
|
||||
|
||||
/// Encode the page contents as UTF-8.
|
||||
///
|
||||
/// If preceding is non-null, then it will be used to initialize our
|
||||
/// blank rows/cells count so that we can accumulate blanks across
|
||||
/// multiple pages.
|
||||
///
|
||||
/// Note: Many tests for this function are done via Screen.dumpString
|
||||
/// tests since that function is a thin wrapper around this one and
|
||||
/// it makes it easier to test input contents.
|
||||
pub fn encodeUtf8(
|
||||
self: *const Page,
|
||||
writer: *std.Io.Writer,
|
||||
opts: EncodeUtf8Options,
|
||||
) anyerror!EncodeUtf8Options.TrailingUtf8State {
|
||||
var blank_rows: usize = opts.preceding.rows;
|
||||
var blank_cells: usize = opts.preceding.cells;
|
||||
|
||||
const start_y: size.CellCountInt = opts.start_y;
|
||||
const end_y: size.CellCountInt = opts.end_y orelse self.size.rows;
|
||||
|
||||
// We can probably avoid this by doing the logic below in a different
|
||||
// way. The reason this exists is so that when we end a non-blank
|
||||
// line with a newline, we can correctly map the cell map over to
|
||||
// the correct x value.
|
||||
//
|
||||
// For example "A\nB". The cell map for "\n" should be (1, 0).
|
||||
// This is tested in Screen.zig so feel free to refactor this.
|
||||
var last_x: size.CellCountInt = 0;
|
||||
|
||||
for (start_y..end_y) |y_usize| {
|
||||
const y: size.CellCountInt = @intCast(y_usize);
|
||||
const row: *Row = self.getRow(y);
|
||||
const cells: []const Cell = self.getCells(row);
|
||||
|
||||
// If this row is blank, accumulate to avoid a bunch of extra
|
||||
// work later. If it isn't blank, make sure we dump all our
|
||||
// blanks.
|
||||
if (!Cell.hasTextAny(cells)) {
|
||||
blank_rows += 1;
|
||||
continue;
|
||||
}
|
||||
for (1..blank_rows + 1) |i| {
|
||||
try writer.writeByte('\n');
|
||||
|
||||
// This is tested in Screen.zig, i.e. one test is
|
||||
// "cell map with newlines"
|
||||
if (opts.cell_map) |cell_map| {
|
||||
try cell_map.map.append(cell_map.alloc, .{
|
||||
.x = last_x,
|
||||
.y = @intCast(y - blank_rows + i - 1),
|
||||
});
|
||||
last_x = 0;
|
||||
}
|
||||
}
|
||||
blank_rows = 0;
|
||||
|
||||
// If we're not wrapped, we always add a newline so after
|
||||
// the row is printed we can add a newline.
|
||||
if (!row.wrap or !opts.unwrap) blank_rows += 1;
|
||||
|
||||
// If the row doesn't continue a wrap then we need to reset
|
||||
// our blank cell count.
|
||||
if (!row.wrap_continuation or !opts.unwrap) blank_cells = 0;
|
||||
|
||||
// Go through each cell and print it
|
||||
for (cells, 0..) |*cell, x_usize| {
|
||||
const x: size.CellCountInt = @intCast(x_usize);
|
||||
|
||||
// Skip spacers
|
||||
switch (cell.wide) {
|
||||
.narrow, .wide => {},
|
||||
.spacer_head, .spacer_tail => continue,
|
||||
}
|
||||
|
||||
// If we have a zero value, then we accumulate a counter. We
|
||||
// only want to turn zero values into spaces if we have a non-zero
|
||||
// char sometime later.
|
||||
if (!cell.hasText()) {
|
||||
blank_cells += 1;
|
||||
continue;
|
||||
}
|
||||
if (blank_cells > 0) {
|
||||
try writer.splatByteAll(' ', blank_cells);
|
||||
if (opts.cell_map) |cell_map| {
|
||||
for (0..blank_cells) |i| try cell_map.map.append(cell_map.alloc, .{
|
||||
.x = @intCast(x - blank_cells + i),
|
||||
.y = y,
|
||||
});
|
||||
}
|
||||
|
||||
blank_cells = 0;
|
||||
}
|
||||
|
||||
switch (cell.content_tag) {
|
||||
.codepoint => {
|
||||
try writer.print("{u}", .{cell.content.codepoint});
|
||||
if (opts.cell_map) |cell_map| {
|
||||
last_x = x + 1;
|
||||
try cell_map.map.append(cell_map.alloc, .{
|
||||
.x = x,
|
||||
.y = y,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
.codepoint_grapheme => {
|
||||
try writer.print("{u}", .{cell.content.codepoint});
|
||||
if (opts.cell_map) |cell_map| {
|
||||
last_x = x + 1;
|
||||
try cell_map.map.append(cell_map.alloc, .{
|
||||
.x = x,
|
||||
.y = y,
|
||||
});
|
||||
}
|
||||
|
||||
for (self.lookupGrapheme(cell).?) |cp| {
|
||||
try writer.print("{u}", .{cp});
|
||||
if (opts.cell_map) |cell_map| try cell_map.map.append(cell_map.alloc, .{
|
||||
.x = x,
|
||||
.y = y,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Unreachable since we do hasText() above
|
||||
.bg_color_palette,
|
||||
.bg_color_rgb,
|
||||
=> unreachable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .rows = blank_rows, .cells = blank_cells };
|
||||
}
|
||||
|
||||
/// Returns the bitset for the dirty bits on this page.
|
||||
///
|
||||
/// The returned value is a DynamicBitSetUnmanaged but it is NOT
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const PageList = terminal.PageList;
|
|||
const Pin = PageList.Pin;
|
||||
const Selection = terminal.Selection;
|
||||
const Screen = terminal.Screen;
|
||||
const PageFormatter = @import("formatter.zig").PageFormatter;
|
||||
|
||||
/// Searches for a term in a PageList structure.
|
||||
///
|
||||
|
|
@ -147,10 +148,10 @@ const SlidingWindow = struct {
|
|||
const MetaBuf = CircBuf(Meta, undefined);
|
||||
const Meta = struct {
|
||||
node: *PageList.List.Node,
|
||||
cell_map: Page.CellMap,
|
||||
cell_map: std.ArrayList(point.Coordinate),
|
||||
|
||||
pub fn deinit(self: *Meta) void {
|
||||
self.cell_map.deinit();
|
||||
pub fn deinit(self: *Meta, alloc: Allocator) void {
|
||||
self.cell_map.deinit(alloc);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -181,14 +182,14 @@ const SlidingWindow = struct {
|
|||
self.data.deinit(self.alloc);
|
||||
|
||||
var meta_it = self.meta.iterator(.forward);
|
||||
while (meta_it.next()) |meta| meta.deinit();
|
||||
while (meta_it.next()) |meta| meta.deinit(self.alloc);
|
||||
self.meta.deinit(self.alloc);
|
||||
}
|
||||
|
||||
/// Clear all data but retain allocated capacity.
|
||||
pub fn clearAndRetainCapacity(self: *SlidingWindow) void {
|
||||
var meta_it = self.meta.iterator(.forward);
|
||||
while (meta_it.next()) |meta| meta.deinit();
|
||||
while (meta_it.next()) |meta| meta.deinit(self.alloc);
|
||||
self.meta.clear();
|
||||
self.data.clear();
|
||||
self.data_offset = 0;
|
||||
|
|
@ -266,15 +267,15 @@ const SlidingWindow = struct {
|
|||
var saved: usize = 0;
|
||||
while (meta_it.next()) |meta| {
|
||||
const needed = self.needle.len - 1 - saved;
|
||||
if (meta.cell_map.map.items.len >= needed) {
|
||||
if (meta.cell_map.items.len >= needed) {
|
||||
// We save up to this meta. We set our data offset
|
||||
// to exactly where it needs to be to continue
|
||||
// searching.
|
||||
self.data_offset = meta.cell_map.map.items.len - needed;
|
||||
self.data_offset = meta.cell_map.items.len - needed;
|
||||
break;
|
||||
}
|
||||
|
||||
saved += meta.cell_map.map.items.len;
|
||||
saved += meta.cell_map.items.len;
|
||||
} else {
|
||||
// If we exited the while loop naturally then we
|
||||
// never got the amount we needed and so there is
|
||||
|
|
@ -296,8 +297,8 @@ const SlidingWindow = struct {
|
|||
var prune_data_len: usize = 0;
|
||||
for (0..prune_count) |_| {
|
||||
const meta = meta_it.next().?;
|
||||
prune_data_len += meta.cell_map.map.items.len;
|
||||
meta.deinit();
|
||||
prune_data_len += meta.cell_map.items.len;
|
||||
meta.deinit(self.alloc);
|
||||
}
|
||||
self.meta.deleteOldest(prune_count);
|
||||
self.data.deleteOldest(prune_data_len);
|
||||
|
|
@ -364,7 +365,7 @@ const SlidingWindow = struct {
|
|||
// match.
|
||||
const meta_count = tl_meta_idx;
|
||||
meta_it.reset();
|
||||
for (0..meta_count) |_| meta_it.next().?.deinit();
|
||||
for (0..meta_count) |_| meta_it.next().?.deinit(self.alloc);
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
assert(meta_it.idx == meta_count);
|
||||
assert(meta_it.next().?.node == tl.node);
|
||||
|
|
@ -396,19 +397,19 @@ const SlidingWindow = struct {
|
|||
// meta_i is the index we expect to find the match in the
|
||||
// cell map within this meta if it contains it.
|
||||
const meta_i = idx - offset.*;
|
||||
if (meta_i >= meta.cell_map.map.items.len) {
|
||||
if (meta_i >= meta.cell_map.items.len) {
|
||||
// This meta doesn't contain the match. This means we
|
||||
// can also prune this set of data because we only look
|
||||
// forward.
|
||||
offset.* += meta.cell_map.map.items.len;
|
||||
offset.* += meta.cell_map.items.len;
|
||||
continue;
|
||||
}
|
||||
|
||||
// We found the meta that contains the start of the match.
|
||||
const map = meta.cell_map.map.items[meta_i];
|
||||
const map = meta.cell_map.items[meta_i];
|
||||
return .{
|
||||
.node = meta.node,
|
||||
.y = map.y,
|
||||
.y = @intCast(map.y),
|
||||
.x = map.x,
|
||||
};
|
||||
}
|
||||
|
|
@ -428,12 +429,9 @@ const SlidingWindow = struct {
|
|||
// Initialize our metadata for the node.
|
||||
var meta: Meta = .{
|
||||
.node = node,
|
||||
.cell_map = .{
|
||||
.alloc = self.alloc,
|
||||
.map = .empty,
|
||||
},
|
||||
.cell_map = .empty,
|
||||
};
|
||||
errdefer meta.deinit();
|
||||
errdefer meta.deinit(self.alloc);
|
||||
|
||||
// This is suboptimal but we need to encode the page once to
|
||||
// temporary memory, and then copy it into our circular buffer.
|
||||
|
|
@ -443,16 +441,20 @@ const SlidingWindow = struct {
|
|||
defer encoded.deinit();
|
||||
|
||||
// Encode the page into the buffer.
|
||||
const page: *const Page = &meta.node.data;
|
||||
_ = page.encodeUtf8(
|
||||
&encoded.writer,
|
||||
.{ .cell_map = &meta.cell_map },
|
||||
) catch {
|
||||
const formatter: PageFormatter = formatter: {
|
||||
var formatter: PageFormatter = .init(&meta.node.data, .plain);
|
||||
formatter.point_map = .{
|
||||
.alloc = self.alloc,
|
||||
.map = &meta.cell_map,
|
||||
};
|
||||
break :formatter formatter;
|
||||
};
|
||||
formatter.format(&encoded.writer) catch {
|
||||
// writer uses anyerror but the only realistic error on
|
||||
// an ArrayList is out of memory.
|
||||
return error.OutOfMemory;
|
||||
};
|
||||
assert(meta.cell_map.map.items.len == encoded.written().len);
|
||||
assert(meta.cell_map.items.len == encoded.written().len);
|
||||
|
||||
// Ensure our buffers are big enough to store what we need.
|
||||
try self.data.ensureUnusedCapacity(self.alloc, encoded.written().len);
|
||||
|
|
@ -476,7 +478,7 @@ const SlidingWindow = struct {
|
|||
// Integrity check: verify our data matches our metadata exactly.
|
||||
var meta_it = self.meta.iterator(.forward);
|
||||
var data_len: usize = 0;
|
||||
while (meta_it.next()) |m| data_len += m.cell_map.map.items.len;
|
||||
while (meta_it.next()) |m| data_len += m.cell_map.items.len;
|
||||
assert(data_len == self.data.len());
|
||||
|
||||
// Integrity check: verify our data offset is within bounds.
|
||||
|
|
|
|||
Loading…
Reference in New Issue