mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-23 21:30:19 +00:00
fix: preserve active cursor position during reflow
This PR fixes an issue where reflowing could leave the active cursor attached to a clipped trailing blank cell instead of following the current write position.
This commit is contained in:
@@ -935,6 +935,10 @@ pub const Resize = struct {
|
||||
pub const Cursor = struct {
|
||||
x: size.CellCountInt,
|
||||
y: size.CellCountInt,
|
||||
|
||||
/// When set, this pin preserves right-side blank cells up to the cursor
|
||||
/// during reflow.
|
||||
pin: ?*Pin = null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1013,10 +1017,6 @@ fn resizeCols(
|
||||
) Allocator.Error!void {
|
||||
assert(cols != self.cols);
|
||||
|
||||
// Update our cols. We have to do this early because grow() that we
|
||||
// may call below relies on this to calculate the proper page size.
|
||||
self.cols = cols;
|
||||
|
||||
// If we have a cursor position (x,y), then we try under any col resizing
|
||||
// to keep the same number remaining active rows beneath it. This is a
|
||||
// very special case if you can imagine clearing the screen (i.e.
|
||||
@@ -1025,10 +1025,11 @@ fn resizeCols(
|
||||
// pull down scrollback.
|
||||
const preserved_cursor: ?struct {
|
||||
tracked_pin: *Pin,
|
||||
untrack: bool,
|
||||
remaining_rows: usize,
|
||||
wrapped_rows: usize,
|
||||
} = if (cursor) |c| cursor: {
|
||||
const p = self.pin(.{ .active = .{
|
||||
const p = if (c.pin) |cursor_pin| cursor_pin.* else self.pin(.{ .active = .{
|
||||
.x = c.x,
|
||||
.y = c.y,
|
||||
} }) orelse break :cursor null;
|
||||
@@ -1051,12 +1052,21 @@ fn resizeCols(
|
||||
};
|
||||
|
||||
break :cursor .{
|
||||
.tracked_pin = try self.trackPin(p),
|
||||
.tracked_pin = c.pin orelse try self.trackPin(p),
|
||||
.untrack = c.pin == null,
|
||||
.remaining_rows = self.rows - c.y - 1,
|
||||
.wrapped_rows = wrapped,
|
||||
};
|
||||
} else null;
|
||||
defer if (preserved_cursor) |c| self.untrackPin(c.tracked_pin);
|
||||
defer if (preserved_cursor) |c| {
|
||||
if (c.untrack) self.untrackPin(c.tracked_pin);
|
||||
};
|
||||
|
||||
// Update our cols. We have to do this early because grow() that we
|
||||
// may call below relies on this to calculate the proper page size, but
|
||||
// after preserved_cursor so that the cursor pin can resolve coordinates in
|
||||
// the old active coordinate space.
|
||||
self.cols = cols;
|
||||
|
||||
// Create the first node that contains our reflow.
|
||||
const first_rewritten_node = node: {
|
||||
@@ -1110,7 +1120,11 @@ fn resizeCols(
|
||||
{
|
||||
var reflow_cursor: ReflowCursor = .init(first_rewritten_node);
|
||||
while (it.next()) |row| {
|
||||
try reflow_cursor.reflowRow(self, row);
|
||||
try reflow_cursor.reflowRow(
|
||||
self,
|
||||
row,
|
||||
if (preserved_cursor) |c| c.tracked_pin else null,
|
||||
);
|
||||
|
||||
// Once we're done reflowing a page, destroy it immediately.
|
||||
// This frees memory and makes it more likely in memory
|
||||
@@ -1226,6 +1240,7 @@ const ReflowCursor = struct {
|
||||
self: *ReflowCursor,
|
||||
list: *PageList,
|
||||
row: Pin,
|
||||
cursor_pin: ?*Pin,
|
||||
) Allocator.Error!void {
|
||||
const src_page: *Page = &row.node.data;
|
||||
const src_row = row.rowAndCell().row;
|
||||
@@ -1253,6 +1268,8 @@ const ReflowCursor = struct {
|
||||
if (&p.node.data != src_page or
|
||||
p.y != src_y) continue;
|
||||
|
||||
if (cursor_pin != null and p == cursor_pin.?) continue;
|
||||
|
||||
// If this pin is in the blanks on the right and past the end
|
||||
// of the dst col width then we move it to the end of the dst
|
||||
// col width instead.
|
||||
@@ -1268,6 +1285,14 @@ const ReflowCursor = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// If the cursor is after blanks on the right, those cells are still
|
||||
// before the next write and must reflow with it.
|
||||
if (cursor_pin) |p| {
|
||||
if (&p.node.data == src_page and p.y == src_y) {
|
||||
cols_len = @max(cols_len, p.x + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Defer processing of blank rows so that blank rows
|
||||
// at the end of the page list are never written.
|
||||
if (cols_len == 0) {
|
||||
|
||||
@@ -1766,7 +1766,11 @@ pub inline fn resize(
|
||||
.rows = opts.rows,
|
||||
.cols = opts.cols,
|
||||
.reflow = opts.reflow,
|
||||
.cursor = .{ .x = self.cursor.x, .y = self.cursor.y },
|
||||
.cursor = .{
|
||||
.x = self.cursor.x,
|
||||
.y = self.cursor.y,
|
||||
.pin = self.cursor.page_pin,
|
||||
},
|
||||
});
|
||||
|
||||
// If we have no scrollback and we shrunk our rows, we must explicitly
|
||||
@@ -7277,6 +7281,41 @@ test "Screen: resize less cols to eliminate wide char with row space" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: resize less cols reflows cursor after wrapped text" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var s = try Screen.init(alloc, .{ .cols = 50, .rows = 7, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
for (0..30) |_| try s.testWriteString("a");
|
||||
|
||||
try testing.expectEqual(@as(usize, 0), s.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 30), s.cursor.x);
|
||||
|
||||
try s.resize(.{ .cols = 25, .rows = 7 });
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), s.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 5), s.cursor.x);
|
||||
}
|
||||
|
||||
test "Screen: resize less cols reflows cursor after empty cells" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var s = try Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
try s.testWriteString("abc");
|
||||
s.cursorRight(6);
|
||||
|
||||
try testing.expectEqual(@as(usize, 0), s.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 9), s.cursor.x);
|
||||
|
||||
try s.resize(.{ .cols = 5, .rows = 3 });
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), s.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 4), s.cursor.x);
|
||||
}
|
||||
|
||||
test "Screen: resize more cols with wide spacer head" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
Reference in New Issue
Block a user