diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 92bb9e690..1af627ada 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1314,28 +1314,24 @@ pub fn print(self: *Terminal, c: u21) !void { if (prev.cell.wide != .wide) break :narrow; prev.cell.wide = .narrow; - // Remove the wide spacer tail - const cell = self.screens.active.cursorCellLeft(prev.left - 1); - cell.wide = .narrow; - - // Back track the cursor so that we don't end up with - // an extra space after the character. Since xterm is - // not VS aware, it cannot be used as a reference for - // this behavior; but it does follow the principle of - // least surprise, and also matches the behavior that - // can be observed in Kitty, which is one of the only - // other VS aware terminals. - if (self.screens.active.cursor.x == right_limit - 1) { - // If we're already at the right edge, we stay - // here and set the pending wrap to false since - // when we pend a wrap, we only move our cursor once - // even for wide chars (tests verify). - self.screens.active.cursor.pending_wrap = false; - } else { - // Otherwise, move back. - self.screens.active.cursorLeft(1); + // Remove the wide spacer tail. The previous cell may be + // under the cursor, so locate the tail from the wide base + // rather than by subtracting from the cursor distance. + const prev_x = self.screens.active.cursor.x - prev.left; + if (prev_x < self.cols - 1) { + const cells: [*]Cell = @ptrCast(prev.cell); + cells[1].wide = .narrow; } + // Place the cursor one cell after the now-narrow base, + // clamped to the right edge. Usually this moves the cursor + // back from after the old tail, but saved cursor state or + // changed margins can leave it directly on the base. + self.screens.active.cursor.pending_wrap = false; + self.screens.active.cursorHorizontalAbsolute( + @min(prev_x + 1, right_limit - 1), + ); + break :narrow; }, @@ -5746,6 +5742,68 @@ test "Terminal: VS15 to make narrow character with pending wrap" { } } +test "Terminal: VS15 narrows wide cell under cursor with wraparound disabled" { + var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 }); + defer t.deinit(testing.allocator); + + t.modes.set(.grapheme_cluster, true); + t.modes.set(.wraparound, false); + + // First create a wide cell spanning columns 4 and 5. + t.setCursorPos(1, 4); + try t.print(0x2614); + + // Make column 4 the right margin and put the cursor on the wide base. + // With wraparound disabled, grapheme lookup selects the cell under the + // cursor when it has content. + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(1, 4); + t.setCursorPos(1, 4); + try t.print(0xFE0E); + + try testing.expectEqual(@as(usize, 3), t.screens.active.cursor.x); + try testing.expect(!t.screens.active.cursor.pending_wrap); + const base = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?.cell; + try testing.expectEqual(Cell.Wide.narrow, base.wide); + try testing.expect(base.hasGrapheme()); + const tail = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?.cell; + try testing.expectEqual(Cell.Wide.narrow, tail.wide); +} + +test "Terminal: VS15 narrows wide cell under restored pending cursor" { + var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 }); + defer t.deinit(testing.allocator); + + t.modes.set(.grapheme_cluster, true); + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(1, 4); + + // Save a pending-wrap cursor at column 4. + t.setCursorPos(1, 4); + try t.print('X'); + try testing.expect(t.screens.active.cursor.pending_wrap); + t.saveCursor(); + + // Widen the margin and replace that cell with a wide character. + t.setLeftAndRightMargin(1, 5); + t.setCursorPos(1, 4); + try t.print(0x2614); + + // Restoring also restores pending_wrap, so grapheme lookup selects the + // wide base under the cursor rather than its spacer tail. + t.restoreCursor(); + try testing.expect(t.screens.active.cursor.pending_wrap); + try t.print(0xFE0E); + + try testing.expectEqual(@as(usize, 4), t.screens.active.cursor.x); + try testing.expect(!t.screens.active.cursor.pending_wrap); + const base = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?.cell; + try testing.expectEqual(Cell.Wide.narrow, base.wide); + try testing.expect(base.hasGrapheme()); + const tail = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?.cell; + try testing.expectEqual(Cell.Wide.narrow, tail.wide); +} + test "Terminal: VS16 to make wide character on next line" { var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 3 }); defer t.deinit(testing.allocator);