a73x

79da83da

test: the grid's wide flags are pinned against ghostty's page cells

a73x   2026-09-04 18:04

Commit message
test: the grid's wide flags are pinned against ghostty's page cells

Deleting `Engine.clipCol` and `Engine.snapWide` took the only cross-check of a
mirrored grid's wide flags against ghostty's own with it. The plain dump cannot
replace it: a spacer mis-tagged as narrow carries the same text and shifts
every clip and every highlight past it by one column.

So the oracle reads `cell.wide` off the page the way `encodeRowAt` reads it and
asks the grid for the same answer at every column of every row. The screen puts
wide glyphs at two different columns on one row and ends another with a glyph
that does not fit its last column, so ghostty leaves a spacer_head there and
wraps the glyph to the row below. A tail assertion refuses a screen that does
not actually hold all four shapes, or the comparison is a loop over narrow
cells that grades nothing.

Watched to fail: mapping the encoder's spacer_head to narrow reports
"wide flag diverged at col 11 row 1: engine says spacer_head, grid says narrow".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsWfuJFQbTfGtKZLS5qw4q

src/engine/engine.zig
Old New
@@ -1704,3 +1704,59 @@ test "grid oracle: the grid's dumpPlain and cursor agree with the engine's for e
1704 try std.testing.expectEqual(e.cursorPos().y, g.cursor.y); 1704 try std.testing.expectEqual(e.cursorPos().y, g.cursor.y);
1705 } 1705 }
1706 } 1706 }
1707
1708 // The wide flag is the half of a cell that `dumpPlain` cannot grade: a spacer
1709 // mis-tagged as narrow carries the same text and shifts every clip and every
1710 // highlight past it by one column. So this reads ghostty's own `cell.wide`
1711 // through the page, the way `encodeRowAt` reads it, and asks the mirrored grid
1712 // for the same answer at every column of every row.
1713 test "grid oracle: every cell's wide flag matches ghostty's page cell" {
1714 const alloc = std.testing.allocator;
1715 const cols: u16 = 12;
1716 const rows: u16 = 3;
1717 var e = try Engine.init(alloc, .{ .cols = cols, .rows = rows });
1718 defer e.deinit();
1719 // Row 0 puts wide glyphs at two different columns, so a rule that only
1720 // works at an even column fails here. Row 1 ends with a wide glyph that
1721 // does not fit its last column: ghostty leaves a spacer_head there and
1722 // wraps the glyph itself onto row 2.
1723 e.feed("ab\u{6f22}cd\u{1F600}z\r\n0123456789a\u{6f22}");
1724 const g = try Grid.init(alloc, 1, 1);
1725 defer g.deinit();
1726 try e.mirrorInto(g);
1727
1728 var seen = [_]bool{false} ** 4;
1729 var y: u16 = 0;
1730 while (y < rows) : (y += 1) {
1731 const screen = e.term.screens.active;
1732 const pin = screen.pages.pin(.{ .viewport = .{ .x = 0, .y = y } }).?;
1733 const page = &pin.node.data;
1734 const cells = page.getCells(pin.rowAndCell().row);
1735 var x: u16 = 0;
1736 while (x < cols) : (x += 1) {
1737 const want: proto.Wide = switch (cells[x].wide) {
1738 .narrow => .narrow,
1739 .wide => .wide,
1740 .spacer_tail => .spacer_tail,
1741 .spacer_head => .spacer_head,
1742 };
1743 seen[@intFromEnum(want)] = true;
1744 const got = g.lines[y].cells[x].wide;
1745 if (got != want) {
1746 std.debug.print(
1747 "wide flag diverged at col {d} row {d}: engine says {s}, grid says {s}\n",
1748 .{ x, y, @tagName(want), @tagName(got) },
1749 );
1750 return error.WideFlagDiverged;
1751 }
1752 }
1753 }
1754 // Without this the loop above could be twelve narrow cells three times over
1755 // and grade nothing: the screen has to hold every shape it claims to.
1756 for (seen, 0..) |s, i| {
1757 if (!s) {
1758 std.debug.print("screen never held a {s} cell\n", .{@tagName(@as(proto.Wide, @enumFromInt(i)))});
1759 return error.ScreenMissesAWideShape;
1760 }
1761 }
1762 }