mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
terminal2: resize cols blank row preservation
This commit is contained in:
@ -7454,6 +7454,7 @@ test "Screen: resize less cols with reflow previously wrapped and scrollback" {
|
|||||||
try testing.expectEqual(@as(usize, 2), s.cursor.y);
|
try testing.expectEqual(@as(usize, 2), s.cursor.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Screen: resize less cols with scrollback keeps cursor row" {
|
test "Screen: resize less cols with scrollback keeps cursor row" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
@ -398,7 +398,7 @@ pub fn resize(self: *PageList, opts: Resize) !void {
|
|||||||
.gt => {
|
.gt => {
|
||||||
// We grow rows after cols so that we can do our unwrapping/reflow
|
// We grow rows after cols so that we can do our unwrapping/reflow
|
||||||
// before we do a no-reflow grow.
|
// before we do a no-reflow grow.
|
||||||
try self.resizeCols(cols);
|
try self.resizeCols(cols, opts.cursor);
|
||||||
try self.resizeWithoutReflow(opts);
|
try self.resizeWithoutReflow(opts);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -411,7 +411,7 @@ pub fn resize(self: *PageList, opts: Resize) !void {
|
|||||||
break :opts copy;
|
break :opts copy;
|
||||||
});
|
});
|
||||||
|
|
||||||
try self.resizeCols(cols);
|
try self.resizeCols(cols, opts.cursor);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -420,12 +420,35 @@ pub fn resize(self: *PageList, opts: Resize) !void {
|
|||||||
fn resizeCols(
|
fn resizeCols(
|
||||||
self: *PageList,
|
self: *PageList,
|
||||||
cols: size.CellCountInt,
|
cols: size.CellCountInt,
|
||||||
|
cursor: ?Resize.Cursor,
|
||||||
) !void {
|
) !void {
|
||||||
assert(cols != self.cols);
|
assert(cols != self.cols);
|
||||||
|
|
||||||
// Our new capacity, ensure we can fit the cols
|
// Our new capacity, ensure we can fit the cols
|
||||||
const cap = try std_capacity.adjust(.{ .cols = cols });
|
const cap = try std_capacity.adjust(.{ .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.
|
||||||
|
// scrollClear), having an empty active area, and then resizing to less
|
||||||
|
// cols then we don't want the active area to "jump" to the bottom and
|
||||||
|
// pull down scrollback.
|
||||||
|
const preserved_cursor: ?struct {
|
||||||
|
tracked_pin: *Pin,
|
||||||
|
remaining_rows: usize,
|
||||||
|
} = if (cursor) |c| cursor: {
|
||||||
|
const p = self.pin(.{ .active = .{
|
||||||
|
.x = c.x,
|
||||||
|
.y = c.y,
|
||||||
|
} }) orelse break :cursor null;
|
||||||
|
|
||||||
|
break :cursor .{
|
||||||
|
.tracked_pin = try self.trackPin(p),
|
||||||
|
.remaining_rows = self.rows - c.y - 1,
|
||||||
|
};
|
||||||
|
} else null;
|
||||||
|
defer if (preserved_cursor) |c| self.untrackPin(c.tracked_pin);
|
||||||
|
|
||||||
// Go page by page and shrink the columns on a per-page basis.
|
// Go page by page and shrink the columns on a per-page basis.
|
||||||
var it = self.pageIterator(.right_down, .{ .screen = .{} }, null);
|
var it = self.pageIterator(.right_down, .{ .screen = .{} }, null);
|
||||||
while (it.next()) |chunk| {
|
while (it.next()) |chunk| {
|
||||||
@ -463,6 +486,39 @@ fn resizeCols(
|
|||||||
for (total..self.rows) |_| _ = try self.grow();
|
for (total..self.rows) |_| _ = try self.grow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See preserved_cursor setup for why.
|
||||||
|
if (preserved_cursor) |c| cursor: {
|
||||||
|
const active_pt = self.pointFromPin(
|
||||||
|
.active,
|
||||||
|
c.tracked_pin.*,
|
||||||
|
) orelse break :cursor;
|
||||||
|
|
||||||
|
// We need to determine how many rows we wrapped from the original
|
||||||
|
// and subtract that from the remaining rows we expect because if
|
||||||
|
// we wrap down we don't want to push our original row contents into
|
||||||
|
// the scrollback.
|
||||||
|
const wrapped = wrapped: {
|
||||||
|
var wrapped: usize = 0;
|
||||||
|
|
||||||
|
var row_it = c.tracked_pin.rowIterator(.left_up, null);
|
||||||
|
_ = row_it.next(); // skip ourselves
|
||||||
|
while (row_it.next()) |next| {
|
||||||
|
const row = next.rowAndCell().row;
|
||||||
|
if (!row.wrap) break;
|
||||||
|
wrapped += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :wrapped wrapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we wrapped more than we expect, do nothing.
|
||||||
|
if (wrapped >= c.remaining_rows) break :cursor;
|
||||||
|
const desired = c.remaining_rows - wrapped;
|
||||||
|
const current = self.rows - (active_pt.active.y + 1);
|
||||||
|
if (current >= desired) break :cursor;
|
||||||
|
for (0..desired - current) |_| _ = try self.grow();
|
||||||
|
}
|
||||||
|
|
||||||
// Update our cols
|
// Update our cols
|
||||||
self.cols = cols;
|
self.cols = cols;
|
||||||
}
|
}
|
||||||
@ -628,17 +684,48 @@ fn reflowPage(
|
|||||||
// row is wrapped then we don't trim trailing empty cells because
|
// row is wrapped then we don't trim trailing empty cells because
|
||||||
// the empty cells can be meaningful.
|
// the empty cells can be meaningful.
|
||||||
const trailing_empty = src_cursor.countTrailingEmptyCells();
|
const trailing_empty = src_cursor.countTrailingEmptyCells();
|
||||||
const cols_len = src_cursor.page.size.cols - trailing_empty;
|
const cols_len = cols_len: {
|
||||||
|
var cols_len = src_cursor.page.size.cols - trailing_empty;
|
||||||
|
if (cols_len > 0) break :cols_len cols_len;
|
||||||
|
|
||||||
if (cols_len == 0) {
|
// If a tracked pin is in this row then we need to keep it
|
||||||
// If the row is empty, we don't copy it. We count it as a
|
// even if it is empty, because it is somehow meaningful
|
||||||
// blank line and continue to the next row.
|
// (usually the screen cursor), but we do trim the cells
|
||||||
blank_lines += 1;
|
// down to the desired size.
|
||||||
continue;
|
//
|
||||||
}
|
// The reason we do this logic is because if you do a scroll
|
||||||
|
// clear (i.e. move all active into scrollback and reset
|
||||||
|
// the screen), the cursor is on the top line again with
|
||||||
|
// an empty active. If you resize to a smaller col size we
|
||||||
|
// don't want to "pull down" all the scrollback again. The
|
||||||
|
// user expects we just shrink the active area.
|
||||||
|
var it = self.tracked_pins.keyIterator();
|
||||||
|
while (it.next()) |p_ptr| {
|
||||||
|
const p = p_ptr.*;
|
||||||
|
if (&p.page.data != src_cursor.page or
|
||||||
|
p.y != src_cursor.y) continue;
|
||||||
|
|
||||||
|
// If our tracked pin is outside our resized cols, we
|
||||||
|
// trim it to the last col, we don't want to wrap blanks.
|
||||||
|
if (p.x >= cap.cols) p.x = cap.cols - 1;
|
||||||
|
|
||||||
|
// We increase our col len to at least include this pin
|
||||||
|
cols_len = @max(cols_len, p.x + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cols_len == 0) {
|
||||||
|
// If the row is empty, we don't copy it. We count it as a
|
||||||
|
// blank line and continue to the next row.
|
||||||
|
blank_lines += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :cols_len cols_len;
|
||||||
|
};
|
||||||
|
|
||||||
// We have data, if we have blank lines we need to create them first.
|
// We have data, if we have blank lines we need to create them first.
|
||||||
for (0..blank_lines) |_| {
|
for (0..blank_lines) |_| {
|
||||||
|
// TODO: cursor in here
|
||||||
dst_cursor.cursorScroll();
|
dst_cursor.cursorScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4775,6 +4862,50 @@ test "PageList resize reflow less cols blank lines between" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "PageList resize reflow less cols cursor not on last line preserves location" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 5, 5, 1);
|
||||||
|
defer s.deinit();
|
||||||
|
try testing.expect(s.pages.first == s.pages.last);
|
||||||
|
const page = &s.pages.first.?.data;
|
||||||
|
for (0..s.rows) |y| {
|
||||||
|
for (0..2) |x| {
|
||||||
|
const rac = page.getRowAndCell(x, y);
|
||||||
|
rac.cell.* = .{
|
||||||
|
.content_tag = .codepoint,
|
||||||
|
.content = .{ .codepoint = @intCast(x) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow blank rows to push our rows back into scrollback
|
||||||
|
try s.growRows(5);
|
||||||
|
try testing.expectEqual(@as(usize, 10), s.totalRows());
|
||||||
|
|
||||||
|
// Put a tracked pin in the history
|
||||||
|
const p = try s.trackPin(s.pin(.{ .active = .{ .x = 0, .y = 0 } }).?);
|
||||||
|
defer s.untrackPin(p);
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
try s.resize(.{
|
||||||
|
.cols = 4,
|
||||||
|
.reflow = true,
|
||||||
|
|
||||||
|
// Important: not on last row
|
||||||
|
.cursor = .{ .x = 1, .y = 1 },
|
||||||
|
});
|
||||||
|
try testing.expectEqual(@as(usize, 4), s.cols);
|
||||||
|
try testing.expectEqual(@as(usize, 10), s.totalRows());
|
||||||
|
|
||||||
|
// Our cursor should move to the first row
|
||||||
|
try testing.expectEqual(point.Point{ .active = .{
|
||||||
|
.x = 0,
|
||||||
|
.y = 0,
|
||||||
|
} }, s.pointFromPin(.active, p.*).?);
|
||||||
|
}
|
||||||
|
|
||||||
test "PageList resize reflow less cols copy style" {
|
test "PageList resize reflow less cols copy style" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
@ -3698,6 +3698,35 @@ test "Screen: resize less cols with reflow previously wrapped and scrollback" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Screen: resize less cols with scrollback keeps cursor row" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 5, 3, 5);
|
||||||
|
defer s.deinit();
|
||||||
|
const str = "1A\n2B\n3C\n4D\n5E";
|
||||||
|
try s.testWriteString(str);
|
||||||
|
|
||||||
|
// Lets do a scroll and clear operation
|
||||||
|
try s.scrollClear();
|
||||||
|
|
||||||
|
// Move our cursor to the beginning
|
||||||
|
s.cursorAbsolute(0, 0);
|
||||||
|
|
||||||
|
try s.resize(3, 3);
|
||||||
|
|
||||||
|
{
|
||||||
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer alloc.free(contents);
|
||||||
|
const expected = "";
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor should be on the last line
|
||||||
|
try testing.expectEqual(@as(size.CellCountInt, 0), s.cursor.x);
|
||||||
|
try testing.expectEqual(@as(size.CellCountInt, 0), s.cursor.y);
|
||||||
|
}
|
||||||
|
|
||||||
test "Screen: resize more rows, less cols with reflow with scrollback" {
|
test "Screen: resize more rows, less cols with reflow with scrollback" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
Reference in New Issue
Block a user