diff --git a/src/terminal/new/Terminal.zig b/src/terminal/new/Terminal.zig index c9dc49bc6..d6d482822 100644 --- a/src/terminal/new/Terminal.zig +++ b/src/terminal/new/Terminal.zig @@ -1299,6 +1299,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // If the original source (now copied to dst) had graphemes, // we have to move them since they're stored by cell offset. if (dst.hasGrapheme()) { + assert(!src.hasGrapheme()); page.moveGraphemeWithinRow(src, dst); } } @@ -1377,7 +1378,6 @@ fn blankCells( for (cells) |*cell| { if (cell.hasGrapheme()) page.clearGrapheme(row, cell); } - assert(!row.grapheme); } if (row.styled) { @@ -4988,6 +4988,74 @@ test "Terminal: insertBlanks left/right scroll region large count" { } } +test "Terminal: insertBlanks deleting graphemes" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + // Disable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + try t.printString("ABC"); + + // This is: 👨‍👩‍👧 (which may or may not render correctly) + try t.print(0x1F468); + try t.print(0x200D); + try t.print(0x1F469); + try t.print(0x200D); + try t.print(0x1F467); + + // We should have one cell with graphemes + const page = t.screen.cursor.page_offset.page.data; + try testing.expectEqual(@as(usize, 1), page.graphemeCount()); + + t.setCursorPos(1, 1); + t.insertBlanks(4); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" A", str); + } + + // We should have no graphemes + try testing.expectEqual(@as(usize, 0), page.graphemeCount()); +} + +test "Terminal: insertBlanks shift graphemes" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + // Disable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + try t.printString("A"); + + // This is: 👨‍👩‍👧 (which may or may not render correctly) + try t.print(0x1F468); + try t.print(0x200D); + try t.print(0x1F469); + try t.print(0x200D); + try t.print(0x1F467); + + // We should have one cell with graphemes + const page = t.screen.cursor.page_offset.page.data; + try testing.expectEqual(@as(usize, 1), page.graphemeCount()); + + t.setCursorPos(1, 1); + t.insertBlanks(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" A👨‍👩‍👧", str); + } + + // We should have no graphemes + try testing.expectEqual(@as(usize, 1), page.graphemeCount()); +} + test "Terminal: insert mode with space" { const alloc = testing.allocator; var t = try init(alloc, 10, 2); diff --git a/src/terminal/new/page.zig b/src/terminal/new/page.zig index c799ee4c6..adc475264 100644 --- a/src/terminal/new/page.zig +++ b/src/terminal/new/page.zig @@ -321,11 +321,30 @@ pub const Page = struct { row.grapheme = false; } + /// Returns the number of graphemes in the page. This isn't the byte + /// size but the total number of unique cells that have grapheme data. + pub fn graphemeCount(self: *const Page) usize { + return self.grapheme_map.map(self.memory).count(); + } + /// Move graphemes to another cell in the same row. pub fn moveGraphemeWithinRow(self: *Page, src: *Cell, dst: *Cell) void { - _ = self; - _ = src; - _ = dst; + // Note: we don't assert src has graphemes here because one of + // the places we call this is from insertBlanks where the cells have + // already swapped cell data but not grapheme data. + + // Get our entry in the map, which must exist + const src_offset = getOffset(Cell, self.memory, src); + var map = self.grapheme_map.map(self.memory); + const entry = map.getEntry(src_offset).?; + const value = entry.value_ptr.*; + + // Remove the entry so we know we have space + map.removeByPtr(entry.key_ptr); + + // Add the entry for the new cell + const dst_offset = getOffset(Cell, self.memory, dst); + map.putAssumeCapacity(dst_offset, value); } pub const Layout = struct {