From a4d54dca1c50ea1a347da796735aacb66d69eaa0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 29 Oct 2025 10:50:47 -0700 Subject: [PATCH] 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. --- src/terminal/PageList.zig | 56 ++---------- src/terminal/Screen.zig | 113 +++++++---------------- src/terminal/page.zig | 187 -------------------------------------- src/terminal/search.zig | 56 ++++++------ 4 files changed, 69 insertions(+), 343 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 3aba29128..82c64591b 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -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; }, }; } diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 486c4f384..5b90bf41b 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -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]); -} diff --git a/src/terminal/page.zig b/src/terminal/page.zig index e38e96e92..5c83fc7c8 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -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 diff --git a/src/terminal/search.zig b/src/terminal/search.zig index d9f6c5663..932ab5a35 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -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.