diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 3bcfdf5d9..05085721a 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -952,7 +952,13 @@ pub fn print(self: *Terminal, c: u21) !void { const cps = self.screens.active.cursor.page_pin.node.page().lookupGrapheme(prev.cell).?; for (cps) |cp2| { // log.debug("cp1={x} cp2={x}", .{ previous_codepoint, cp2 }); - assert(!unicode.graphemeBreak(previous_codepoint, cp2, &state)); + // With mode 2027 disabled, zero-width codepoints are + // attached without applying grapheme boundary rules. If + // the mode is enabled later, an existing cell can + // therefore contain one or more breaks. Feed those breaks + // into the state machine so it can reset its context and + // determine the boundary for the new codepoint. + _ = unicode.graphemeBreak(previous_codepoint, cp2, &state); previous_codepoint = cp2; } } @@ -4297,6 +4303,22 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" { try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } +test "Terminal: enabling grapheme mode handles stored breaks" { + var t = try init(testing.allocator, .{ .cols = 5, .rows = 1 }); + defer t.deinit(testing.allocator); + + t.modes.set(.grapheme_cluster, false); + try t.print('a'); + try t.print(0x200B); // Zero width space is stored on the prior cell. + + t.modes.set(.grapheme_cluster, true); + try t.print(0x0301); + + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("a\xE2\x80\x8B\xCC\x81", str); +} + // Terminal.print receives one codepoint at a time, so it can't use // unicode.graphemeWidth directly; that API requires a complete buffered // cluster or string end. This keeps the streaming printer's cursor advance @@ -12491,13 +12513,6 @@ fn testPrintSliceDifferential( }, 15 => { const v = rand.boolean(); - // Erase the display first: grapheme clusters created - // while mode 2027 was off can trip a pre-existing - // debug assert in print()'s cluster walk when the mode - // is toggled on (unrelated to printSlice; it reproduces - // with per-codepoint print alone). - t1.eraseDisplay(.complete, false); - t2.eraseDisplay(.complete, false); t1.modes.set(.grapheme_cluster, v); t2.modes.set(.grapheme_cluster, v); },