terminal/new: non-grapheme zwjs

This commit is contained in:
Mitchell Hashimoto
2024-02-23 21:26:06 -08:00
parent de0eb859df
commit 26b1a00380
4 changed files with 176 additions and 25 deletions

View File

@ -2410,6 +2410,7 @@ test "Terminal: VS16 repeated with mode 2027" {
} }
} }
// X
test "Terminal: VS16 doesn't make character with 2027 disabled" { test "Terminal: VS16 doesn't make character with 2027 disabled" {
var t = try init(testing.allocator, 5, 5); var t = try init(testing.allocator, 5, 5);
defer t.deinit(testing.allocator); 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" { test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
var t = try init(testing.allocator, 80, 80); var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator); defer t.deinit(testing.allocator);
@ -2526,6 +2528,7 @@ test "Terminal: print multicodepoint grapheme, mode 2027" {
} }
} }
// X
test "Terminal: print invalid VS16 non-grapheme" { test "Terminal: print invalid VS16 non-grapheme" {
var t = try init(testing.allocator, 80, 80); var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator); defer t.deinit(testing.allocator);

View File

@ -87,10 +87,10 @@ pub fn cursorCellRight(self: *Screen) *pagepkg.Cell {
return @ptrCast(cell + 1); return @ptrCast(cell + 1);
} }
pub fn cursorCellLeft(self: *Screen) *pagepkg.Cell { pub fn cursorCellLeft(self: *Screen, n: size.CellCountInt) *pagepkg.Cell {
assert(self.cursor.x > 0); assert(self.cursor.x >= n);
const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell);
return @ptrCast(cell - 1); return @ptrCast(cell - n);
} }
pub fn cursorCellEndOfPrev(self: *Screen) *pagepkg.Cell { pub fn cursorCellEndOfPrev(self: *Screen) *pagepkg.Cell {
@ -245,7 +245,7 @@ pub fn dumpString(
blank_rows += 1; blank_rows += 1;
var blank_cells: usize = 0; var blank_cells: usize = 0;
for (cells) |cell| { for (cells) |*cell| {
// Skip spacers // Skip spacers
switch (cell.wide) { switch (cell.wide) {
.narrow, .wide => {}, .narrow, .wide => {},
@ -265,6 +265,13 @@ pub fn dumpString(
} }
try writer.print("{u}", .{cell.codepoint}); 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});
}
}
} }
} }
} }

View File

@ -259,7 +259,26 @@ pub fn print(self: *Terminal, c: u21) !void {
return; 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 // We have a printable character, save it
@ -359,7 +378,7 @@ fn printCell(
.spacer_tail => { .spacer_tail => {
assert(self.screen.cursor.x > 0); 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 }; wide_cell.* = .{ .style_id = self.screen.cursor.style_id };
if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) { if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) {
const head_cell = self.screen.cursorCellEndOfPrev(); const head_cell = self.screen.cursorCellEndOfPrev();
@ -391,7 +410,7 @@ fn printCell(
} }
fn printWrap(self: *Terminal) !void { 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 // 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 // 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 // New line must inherit semantic prompt of the old line
// const new_row = self.screen.getRow(.{ .active = self.screen.cursor.y }); // const new_row = self.screen.getRow(.{ .active = self.screen.cursor.y });
// new_row.setSemanticPrompt(old_prompt); // 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. /// 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 our previous line is not wrapped then we are done.
if (wrap_mode != .reverse_extended) { 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); 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" { test "Terminal: soft wrap" {
var t = try init(testing.allocator, 3, 80); var t = try init(testing.allocator, 3, 80);
defer t.deinit(testing.allocator); defer t.deinit(testing.allocator);

View File

@ -395,27 +395,24 @@ pub const Capacity = struct {
}; };
pub const Row = packed struct(u64) { pub const Row = packed struct(u64) {
_padding: u29 = 0,
/// The cells in the row offset from the page. /// The cells in the row offset from the page.
cells: Offset(Cell), cells: Offset(Cell),
/// Flags where we want to pack bits /// True if this row is soft-wrapped. The first cell of the next
flags: packed struct { /// row is a continuation of this row.
/// True if this row is soft-wrapped. The first cell of the next wrap: bool = false,
/// row is a continuation of this row.
wrap: bool = false,
/// True if the previous row to this one is soft-wrapped and /// True if the previous row to this one is soft-wrapped and
/// this row is a continuation of that row. /// this row is a continuation of that row.
wrap_continuation: bool = false, wrap_continuation: bool = false,
/// True if any of the cells in this row have multi-codepoint /// True if any of the cells in this row have multi-codepoint
/// grapheme clusters. If this is true, some fast paths are not /// grapheme clusters. If this is true, some fast paths are not
/// possible because erasing for example may need to clear existing /// possible because erasing for example may need to clear existing
/// grapheme data. /// grapheme data.
grapheme: bool = false, grapheme: bool = false,
} = .{},
_padding: u29 = 0,
}; };
/// A cell represents a single terminal grid cell. /// A cell represents a single terminal grid cell.