mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-26 09:01:44 +00:00
terminal: avoid VS15 cursor underflow (#13631)
Handle VS15 width changes when the wide grapheme base is directly under the cursor. A zero cursor distance previously underflowed while locating the spacer tail. Debug builds panicked and ReleaseFast computed an out-of-bounds cell pointer before updating it.
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user