From be27b825f3bac659c7283ed130c4c9440ea74d8a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Aug 2023 08:18:03 -0700 Subject: [PATCH] terminal: resize with less cols preserves zero width codepoints --- src/terminal/Screen.zig | 56 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 1c3d86362..335cf06c8 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2349,7 +2349,8 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void { errdefer self.storage.deinit(self.alloc); defer old.storage.deinit(self.alloc); - // Copy grapheme map + // Create empty grapheme map. Cell IDs change so we can't just copy it, + // we'll rebuild it. self.graphemes = .{}; errdefer self.deinitGraphemes(); defer old.deinitGraphemes(); @@ -2388,9 +2389,12 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void { } // Fast path: our old row is not wrapped AND our old row fits - // into our new smaller size. In this case, we just do a fast - // copy and move on. - if (!old_row_wrapped and trimmed_row.len <= self.cols) { + // into our new smaller size AND this row has no grapheme clusters. + // In this case, we just do a fast copy and move on. + if (!old_row_wrapped and + trimmed_row.len <= self.cols and + !old_row.header().flags.grapheme) + { // If our cursor is on this line, then set the new cursor. if (cursor_pos.y == old_y) { assert(new_cursor == null); @@ -2473,6 +2477,14 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void { // Write the cell var new_cell = row.getCellPtr(x); new_cell.* = cell.cell; + + // If the old cell is a multi-codepoint grapheme then we + // need to also attach the graphemes. + if (cell.cell.attrs.grapheme) { + var it = cur_old_row.codepointIterator(old_x); + while (it.next()) |cp| try row.attachGrapheme(x, cp); + } + x += 1; } @@ -5625,14 +5637,16 @@ test "Screen: resize less cols with graphemes" { try testing.expectEqual(cursor, s.cursor); { + const expected = "1️A️B️\n2️E️F️\n3️I️J️"; var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); + try testing.expectEqualStrings(expected, contents); } { + const expected = "1️A️B️\n2️E️F️\n3️I️J️"; var contents = try s.testString(alloc, .screen); defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); + try testing.expectEqualStrings(expected, contents); } } @@ -6105,6 +6119,36 @@ test "Screen: resize more cols with wide spacer head" { } } +test "Screen: resize less cols preserves grapheme cluster" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 1, 5, 0); + defer s.deinit(); + const str: []const u8 = &.{ 0x43, 0xE2, 0x83, 0x90 }; // C⃐ (C with combining left arrow) + try s.testWriteString(str); + + // We should have a single cell with all the codepoints + { + const row = s.getRow(.{ .screen = 0 }); + try testing.expectEqual(@as(usize, 2), row.codepointLen(0)); + } + { + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } + + // Resize to less columns. No wrapping, but we should still have + // the same grapheme cluster. + try s.resize(1, 4); + { + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } +} + test "Screen: resize more cols with wide spacer head multiple lines" { const testing = std.testing; const alloc = testing.allocator;