mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
terminal: pagelist resize handles soft-wrap across pages
This commit is contained in:
@ -536,18 +536,22 @@ fn resizeCols(
|
||||
// Fast-path: none of our rows are wrapped. In this case we can
|
||||
// treat this like a no-reflow resize. This only applies if we
|
||||
// are growing columns.
|
||||
if (cols > self.cols) {
|
||||
if (cols > self.cols) no_reflow: {
|
||||
const page = &chunk.page.data;
|
||||
const rows = page.rows.ptr(page.memory)[0..page.size.rows];
|
||||
const wrapped = wrapped: for (rows) |row| {
|
||||
assert(!row.wrap_continuation); // TODO
|
||||
if (row.wrap) break :wrapped true;
|
||||
} else false;
|
||||
if (!wrapped) {
|
||||
|
||||
// If our first row is a wrap continuation, then we have to
|
||||
// reflow since we're continuing a wrapped line.
|
||||
if (rows[0].wrap_continuation) break :no_reflow;
|
||||
|
||||
// If any row is soft-wrapped then we have to reflow
|
||||
for (rows) |row| {
|
||||
if (row.wrap) break :no_reflow;
|
||||
}
|
||||
|
||||
try self.resizeWithoutReflowGrowCols(cap, chunk);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: we can do a fast-path here if all of our rows in this
|
||||
// page already fit within the new capacity. In that case we can
|
||||
@ -628,6 +632,12 @@ const ReflowCursor = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// True if this cursor is at the bottom of the page by capacity,
|
||||
/// i.e. we can't scroll anymore.
|
||||
fn bottom(self: *const ReflowCursor) bool {
|
||||
return self.y == self.page.capacity.rows - 1;
|
||||
}
|
||||
|
||||
fn cursorForward(self: *ReflowCursor) void {
|
||||
if (self.x == self.page.size.cols - 1) {
|
||||
self.pending_wrap = true;
|
||||
@ -638,6 +648,11 @@ const ReflowCursor = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn cursorDown(self: *ReflowCursor) void {
|
||||
assert(self.y + 1 < self.page.size.rows);
|
||||
self.cursorAbsolute(self.x, self.y + 1);
|
||||
}
|
||||
|
||||
fn cursorScroll(self: *ReflowCursor) void {
|
||||
// Scrolling requires that we're on the bottom of our page.
|
||||
// We also assert that we have capacity because reflow always
|
||||
@ -708,18 +723,17 @@ const ReflowCursor = struct {
|
||||
///
|
||||
/// Note a couple edge cases:
|
||||
///
|
||||
/// 1. If the first set of rows of this page are a wrap continuation, then
|
||||
/// we will reflow the continuation rows but will not traverse back to
|
||||
/// find the initial wrap.
|
||||
/// 1. All initial rows that are wrap continuations are ignored. If you
|
||||
/// want to reflow these lines you must reflow the page with the
|
||||
/// initially wrapped line.
|
||||
///
|
||||
/// 2. If the last row is wrapped then we will traverse forward to reflow
|
||||
/// all the continuation rows.
|
||||
/// all the continuation rows. This follows from #1.
|
||||
///
|
||||
/// As a result of the above edge cases, the pagelist may end up removing
|
||||
/// an indefinite number of pages. In the most pathological cases (the screen
|
||||
/// is one giant wrapped line), this can be a very expensive operation. That
|
||||
/// doesn't really happen in typical terminal usage so its not a case we
|
||||
/// optimize for today. Contributions welcome to optimize this.
|
||||
/// Despite the edge cases above, this will only ever remove the initial
|
||||
/// node, so that this can be called within a pageIterator. This is a weird
|
||||
/// detail that will surely cause bugs one day so we should look into fixing
|
||||
/// it. :)
|
||||
///
|
||||
/// Conceptually, this is a simple process: we're effectively traversing
|
||||
/// the old page and rewriting into the new page as if it were a text editor.
|
||||
@ -729,17 +743,37 @@ const ReflowCursor = struct {
|
||||
fn reflowPage(
|
||||
self: *PageList,
|
||||
cap: Capacity,
|
||||
node: *List.Node,
|
||||
initial_node: *List.Node,
|
||||
) !void {
|
||||
// The cursor tracks where we are in the source page.
|
||||
var src_cursor = ReflowCursor.init(&node.data);
|
||||
var src_node = initial_node;
|
||||
var src_cursor = ReflowCursor.init(&src_node.data);
|
||||
|
||||
// This is set to true when we're in the middle of completing a wrap
|
||||
// from the initial page. If this is true, the moment we see a non-wrapped
|
||||
// row we are done.
|
||||
var src_completing_wrap = false;
|
||||
|
||||
// This is used to count blank lines so that we don't copy those.
|
||||
var blank_lines: usize = 0;
|
||||
|
||||
// Skip initially reflowed lines
|
||||
if (src_cursor.page_row.wrap_continuation) {
|
||||
while (src_cursor.page_row.wrap_continuation) {
|
||||
// If this entire page was continuations then we can remove it.
|
||||
if (src_cursor.y == src_cursor.page.size.rows - 1) {
|
||||
self.pages.remove(initial_node);
|
||||
self.destroyPage(initial_node);
|
||||
return;
|
||||
}
|
||||
|
||||
src_cursor.cursorDown();
|
||||
}
|
||||
}
|
||||
|
||||
// Our new capacity when growing columns may also shrink rows. So we
|
||||
// need to do a loop in order to potentially make multiple pages.
|
||||
while (true) {
|
||||
dst_loop: while (true) {
|
||||
// Create our new page and our cursor restarts at 0,0 in the new page.
|
||||
// The new page always starts with a size of 1 because we know we have
|
||||
// at least one row to copy from the src.
|
||||
@ -753,11 +787,17 @@ fn reflowPage(
|
||||
|
||||
// Our new page goes before our src node. This will append it to any
|
||||
// previous pages we've created.
|
||||
self.pages.insertBefore(node, dst_node);
|
||||
self.pages.insertBefore(initial_node, dst_node);
|
||||
|
||||
src_loop: while (true) {
|
||||
// Continue traversing the source until we're out of space in our
|
||||
// destination or we've copied all our intended rows.
|
||||
const started_completing_wrap = src_completing_wrap;
|
||||
for (src_cursor.y..src_cursor.page.size.rows) |src_y| {
|
||||
// If we started completing a wrap and our flag is no longer true
|
||||
// then we completed it and we can exit the loop.
|
||||
if (started_completing_wrap and !src_completing_wrap) break;
|
||||
|
||||
const prev_wrap = src_cursor.page_row.wrap;
|
||||
src_cursor.cursorAbsolute(0, @intCast(src_y));
|
||||
|
||||
@ -805,7 +845,14 @@ fn reflowPage(
|
||||
};
|
||||
|
||||
// We have data, if we have blank lines we need to create them first.
|
||||
for (0..blank_lines) |_| {
|
||||
for (0..blank_lines) |i| {
|
||||
// If we're at the bottom we can't fit anymore into this page,
|
||||
// so we need to reloop and create a new page.
|
||||
if (dst_cursor.bottom()) {
|
||||
blank_lines -= i;
|
||||
continue :dst_loop;
|
||||
}
|
||||
|
||||
// TODO: cursor in here
|
||||
dst_cursor.cursorScroll();
|
||||
}
|
||||
@ -829,7 +876,7 @@ fn reflowPage(
|
||||
for (src_cursor.x..cols_len) |src_x| {
|
||||
assert(src_cursor.x == src_x);
|
||||
|
||||
// std.log.warn("src_y={} src_x={} dst_y={} dst_x={} cp={u}", .{
|
||||
// std.log.warn("src_y={} src_x={} dst_y={} dst_x={} cp={}", .{
|
||||
// src_cursor.y,
|
||||
// src_cursor.x,
|
||||
// dst_cursor.y,
|
||||
@ -958,6 +1005,13 @@ fn reflowPage(
|
||||
// case, if our cursor is in one of the blanks, we update it
|
||||
// to the edge of this page.
|
||||
|
||||
// If we are in wrap completion mode and this row is not wrapped
|
||||
// then we are done and we can gracefully exit our y loop.
|
||||
if (src_completing_wrap and !src_cursor.page_row.wrap) {
|
||||
assert(started_completing_wrap);
|
||||
src_completing_wrap = false;
|
||||
}
|
||||
|
||||
// If we have no trailing empty cells, it can't be in the blanks.
|
||||
if (trailing_empty == 0) break :cursor;
|
||||
|
||||
@ -973,15 +1027,24 @@ fn reflowPage(
|
||||
p.y = dst_cursor.y;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We made it through all our source rows, we're done.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, remove the old page.
|
||||
self.pages.remove(node);
|
||||
self.destroyPage(node);
|
||||
// If we're still in a wrapped line at the end of our page,
|
||||
// we traverse forward and continue reflowing until we complete
|
||||
// this entire line.
|
||||
if (src_cursor.page_row.wrap) {
|
||||
src_completing_wrap = true;
|
||||
src_node = src_node.next.?;
|
||||
src_cursor = ReflowCursor.init(&src_node.data);
|
||||
continue :src_loop;
|
||||
}
|
||||
|
||||
// We are not on a wrapped line, we're truly done.
|
||||
self.pages.remove(initial_node);
|
||||
self.destroyPage(initial_node);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This updates the cursor offset if the cursor is exactly on the cell
|
||||
@ -4183,6 +4246,80 @@ test "PageList resize reflow more cols wrapped rows" {
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList resize reflow more cols wrap across page boundary" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 2, 10, 0);
|
||||
defer s.deinit();
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
|
||||
// Grow to the capacity of the first page.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
for (page.size.rows..page.capacity.rows) |_| {
|
||||
_ = try s.grow();
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
try s.growRows(1);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalPages());
|
||||
}
|
||||
|
||||
// At this point, we have some rows on the first page, and some on the second.
|
||||
// We can now wrap across the boundary condition.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
const y = page.size.rows - 1;
|
||||
{
|
||||
const rac = page.getRowAndCell(0, y);
|
||||
rac.row.wrap = true;
|
||||
}
|
||||
for (0..s.cols) |x| {
|
||||
const rac = page.getRowAndCell(x, y);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = @intCast(x) },
|
||||
};
|
||||
}
|
||||
}
|
||||
{
|
||||
const page2 = &s.pages.last.?.data;
|
||||
const y = 0;
|
||||
{
|
||||
const rac = page2.getRowAndCell(0, y);
|
||||
rac.row.wrap_continuation = true;
|
||||
}
|
||||
for (0..s.cols) |x| {
|
||||
const rac = page2.getRowAndCell(x, y);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = @intCast(x) },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// We expect one extra row since we unwrapped a row we need to resize
|
||||
// to make our active area.
|
||||
const end_rows = s.totalRows();
|
||||
|
||||
// Resize
|
||||
try s.resize(.{ .cols = 4, .reflow = true });
|
||||
try testing.expectEqual(@as(usize, 4), s.cols);
|
||||
try testing.expectEqual(@as(usize, end_rows), s.totalRows());
|
||||
|
||||
{
|
||||
const p = s.pin(.{ .active = .{ .y = 9 } }).?;
|
||||
const row = p.rowAndCell().row;
|
||||
try testing.expect(!row.wrap);
|
||||
|
||||
const cells = p.cells(.all);
|
||||
try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint);
|
||||
try testing.expectEqual(@as(u21, 1), cells[1].content.codepoint);
|
||||
try testing.expectEqual(@as(u21, 0), cells[2].content.codepoint);
|
||||
try testing.expectEqual(@as(u21, 1), cells[3].content.codepoint);
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList resize reflow more cols cursor in wrapped row" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
Reference in New Issue
Block a user