terminal: pagelist resize handles soft-wrap across pages

This commit is contained in:
Mitchell Hashimoto
2024-03-10 09:59:24 -07:00
parent 9c2a5bccc1
commit 9830aacc1c

View File

@ -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;