From 26b1a00380386da939e0b1955bda87c02ad3d0ab Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Feb 2024 21:26:06 -0800 Subject: [PATCH] terminal/new: non-grapheme zwjs --- src/terminal/Terminal.zig | 3 + src/terminal/new/Screen.zig | 15 +++- src/terminal/new/Terminal.zig | 154 ++++++++++++++++++++++++++++++++-- src/terminal/new/page.zig | 29 +++---- 4 files changed, 176 insertions(+), 25 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 6ab2e3f65..d32051be0 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2410,6 +2410,7 @@ test "Terminal: VS16 repeated with mode 2027" { } } +// X test "Terminal: VS16 doesn't make character with 2027 disabled" { var t = try init(testing.allocator, 5, 5); defer t.deinit(testing.allocator); @@ -2435,6 +2436,7 @@ test "Terminal: VS16 doesn't make character with 2027 disabled" { } } +// X test "Terminal: print multicodepoint grapheme, disabled mode 2027" { var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); @@ -2526,6 +2528,7 @@ test "Terminal: print multicodepoint grapheme, mode 2027" { } } +// X test "Terminal: print invalid VS16 non-grapheme" { var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index 7857b252c..43379a57f 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -87,10 +87,10 @@ pub fn cursorCellRight(self: *Screen) *pagepkg.Cell { return @ptrCast(cell + 1); } -pub fn cursorCellLeft(self: *Screen) *pagepkg.Cell { - assert(self.cursor.x > 0); +pub fn cursorCellLeft(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { + assert(self.cursor.x >= n); const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); - return @ptrCast(cell - 1); + return @ptrCast(cell - n); } pub fn cursorCellEndOfPrev(self: *Screen) *pagepkg.Cell { @@ -245,7 +245,7 @@ pub fn dumpString( blank_rows += 1; var blank_cells: usize = 0; - for (cells) |cell| { + for (cells) |*cell| { // Skip spacers switch (cell.wide) { .narrow, .wide => {}, @@ -265,6 +265,13 @@ pub fn dumpString( } try writer.print("{u}", .{cell.codepoint}); + + if (cell.grapheme) { + const cps = row_offset.page.data.lookupGrapheme(cell).?; + for (cps) |cp| { + try writer.print("{u}", .{cp}); + } + } } } } diff --git a/src/terminal/new/Terminal.zig b/src/terminal/new/Terminal.zig index 63cf3f6c9..da5f63c13 100644 --- a/src/terminal/new/Terminal.zig +++ b/src/terminal/new/Terminal.zig @@ -259,7 +259,26 @@ pub fn print(self: *Terminal, c: u21) !void { return; } - @panic("TODO: zero-width characters"); + // Find our previous cell + const prev = prev: { + const immediate = self.screen.cursorCellLeft(1); + if (immediate.wide != .spacer_tail) break :prev immediate; + break :prev self.screen.cursorCellLeft(2); + }; + + // If this is a emoji variation selector, prev must be an emoji + if (c == 0xFE0F or c == 0xFE0E) { + const prev_props = unicode.getProperties(prev.codepoint); + const emoji = prev_props.grapheme_boundary_class == .extended_pictographic; + if (!emoji) return; + } + + try self.screen.cursor.page_offset.page.data.appendGrapheme( + self.screen.cursor.page_row, + prev, + c, + ); + return; } // We have a printable character, save it @@ -359,7 +378,7 @@ fn printCell( .spacer_tail => { assert(self.screen.cursor.x > 0); - const wide_cell = self.screen.cursorCellLeft(); + const wide_cell = self.screen.cursorCellLeft(1); wide_cell.* = .{ .style_id = self.screen.cursor.style_id }; if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) { const head_cell = self.screen.cursorCellEndOfPrev(); @@ -391,7 +410,7 @@ fn printCell( } fn printWrap(self: *Terminal) !void { - self.screen.cursor.page_row.flags.wrap = true; + self.screen.cursor.page_row.wrap = true; // Get the old semantic prompt so we can extend it to the next // line. We need to do this before we index() because we may @@ -407,7 +426,7 @@ fn printWrap(self: *Terminal) !void { // New line must inherit semantic prompt of the old line // const new_row = self.screen.getRow(.{ .active = self.screen.cursor.y }); // new_row.setSemanticPrompt(old_prompt); - self.screen.cursor.page_row.flags.wrap_continuation = true; + self.screen.cursor.page_row.wrap_continuation = true; } /// Carriage return moves the cursor to the first column. @@ -526,7 +545,7 @@ pub fn cursorLeft(self: *Terminal, count_req: usize) void { // If our previous line is not wrapped then we are done. if (wrap_mode != .reverse_extended) { - if (!self.screen.cursor.page_row.flags.wrap) break; + if (!self.screen.cursor.page_row.wrap) break; } self.screen.cursorAbsolute(right_margin, self.screen.cursor.y - 1); @@ -901,6 +920,131 @@ test "Terminal: print over wide spacer tail" { } } +test "Terminal: print multicodepoint grapheme, disabled mode 2027" { + var t = try init(testing.allocator, 80, 80); + defer t.deinit(testing.allocator); + + // https://github.com/mitchellh/ghostty/issues/289 + // 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 6 cells taken up + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 6), t.screen.cursor.x); + + // Assert various properties about our screen to verify + // we have all expected cells. + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x1F468), cell.codepoint); + try testing.expect(cell.grapheme); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + const cps = list_cell.page.data.lookupGrapheme(cell).?; + try testing.expectEqual(@as(usize, 1), cps.len); + } + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, ' '), cell.codepoint); + try testing.expect(!cell.grapheme); + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); + } + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x1F469), cell.codepoint); + try testing.expect(cell.grapheme); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + const cps = list_cell.page.data.lookupGrapheme(cell).?; + try testing.expectEqual(@as(usize, 1), cps.len); + } + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, ' '), cell.codepoint); + try testing.expect(!cell.grapheme); + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); + } + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x1F467), cell.codepoint); + try testing.expect(!cell.grapheme); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); + } + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 5, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, ' '), cell.codepoint); + try testing.expect(!cell.grapheme); + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); + } +} + +test "Terminal: VS16 doesn't make character with 2027 disabled" { + var t = try init(testing.allocator, 5, 5); + defer t.deinit(testing.allocator); + + // Disable grapheme clustering + t.modes.set(.grapheme_cluster, false); + + try t.print(0x2764); // Heart + try t.print(0xFE0F); // VS16 to make wide + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("❤️", str); + } + + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x2764), cell.codepoint); + try testing.expect(cell.grapheme); + try testing.expectEqual(Cell.Wide.narrow, cell.wide); + const cps = list_cell.page.data.lookupGrapheme(cell).?; + try testing.expectEqual(@as(usize, 1), cps.len); + } +} + +test "Terminal: print invalid VS16 non-grapheme" { + var t = try init(testing.allocator, 80, 80); + defer t.deinit(testing.allocator); + + // https://github.com/mitchellh/ghostty/issues/1482 + try t.print('x'); + try t.print(0xFE0F); + + // We should have 2 cells taken up. It is one character but "wide". + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); + + // Assert various properties about our screen to verify + // we have all expected cells. + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 'x'), cell.codepoint); + try testing.expect(!cell.grapheme); + try testing.expectEqual(Cell.Wide.narrow, cell.wide); + } + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.codepoint); + } +} + test "Terminal: soft wrap" { var t = try init(testing.allocator, 3, 80); defer t.deinit(testing.allocator); diff --git a/src/terminal/new/page.zig b/src/terminal/new/page.zig index 96d678f80..ef696adec 100644 --- a/src/terminal/new/page.zig +++ b/src/terminal/new/page.zig @@ -395,27 +395,24 @@ pub const Capacity = struct { }; pub const Row = packed struct(u64) { - _padding: u29 = 0, - /// The cells in the row offset from the page. cells: Offset(Cell), - /// Flags where we want to pack bits - flags: packed struct { - /// True if this row is soft-wrapped. The first cell of the next - /// row is a continuation of this row. - wrap: bool = false, + /// True if this row is soft-wrapped. The first cell of the next + /// row is a continuation of this row. + wrap: bool = false, - /// True if the previous row to this one is soft-wrapped and - /// this row is a continuation of that row. - wrap_continuation: bool = false, + /// True if the previous row to this one is soft-wrapped and + /// this row is a continuation of that row. + wrap_continuation: bool = false, - /// True if any of the cells in this row have multi-codepoint - /// grapheme clusters. If this is true, some fast paths are not - /// possible because erasing for example may need to clear existing - /// grapheme data. - grapheme: bool = false, - } = .{}, + /// True if any of the cells in this row have multi-codepoint + /// grapheme clusters. If this is true, some fast paths are not + /// possible because erasing for example may need to clear existing + /// grapheme data. + grapheme: bool = false, + + _padding: u29 = 0, }; /// A cell represents a single terminal grid cell.