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,17 +536,21 @@ fn resizeCols(
|
|||||||
// Fast-path: none of our rows are wrapped. In this case we can
|
// 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
|
// treat this like a no-reflow resize. This only applies if we
|
||||||
// are growing columns.
|
// are growing columns.
|
||||||
if (cols > self.cols) {
|
if (cols > self.cols) no_reflow: {
|
||||||
const page = &chunk.page.data;
|
const page = &chunk.page.data;
|
||||||
const rows = page.rows.ptr(page.memory)[0..page.size.rows];
|
const rows = page.rows.ptr(page.memory)[0..page.size.rows];
|
||||||
const wrapped = wrapped: for (rows) |row| {
|
|
||||||
assert(!row.wrap_continuation); // TODO
|
// If our first row is a wrap continuation, then we have to
|
||||||
if (row.wrap) break :wrapped true;
|
// reflow since we're continuing a wrapped line.
|
||||||
} else false;
|
if (rows[0].wrap_continuation) break :no_reflow;
|
||||||
if (!wrapped) {
|
|
||||||
try self.resizeWithoutReflowGrowCols(cap, chunk);
|
// If any row is soft-wrapped then we have to reflow
|
||||||
continue;
|
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
|
// Note: we can do a fast-path here if all of our rows in this
|
||||||
@ -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 {
|
fn cursorForward(self: *ReflowCursor) void {
|
||||||
if (self.x == self.page.size.cols - 1) {
|
if (self.x == self.page.size.cols - 1) {
|
||||||
self.pending_wrap = true;
|
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 {
|
fn cursorScroll(self: *ReflowCursor) void {
|
||||||
// Scrolling requires that we're on the bottom of our page.
|
// Scrolling requires that we're on the bottom of our page.
|
||||||
// We also assert that we have capacity because reflow always
|
// We also assert that we have capacity because reflow always
|
||||||
@ -708,18 +723,17 @@ const ReflowCursor = struct {
|
|||||||
///
|
///
|
||||||
/// Note a couple edge cases:
|
/// Note a couple edge cases:
|
||||||
///
|
///
|
||||||
/// 1. If the first set of rows of this page are a wrap continuation, then
|
/// 1. All initial rows that are wrap continuations are ignored. If you
|
||||||
/// we will reflow the continuation rows but will not traverse back to
|
/// want to reflow these lines you must reflow the page with the
|
||||||
/// find the initial wrap.
|
/// initially wrapped line.
|
||||||
///
|
///
|
||||||
/// 2. If the last row is wrapped then we will traverse forward to reflow
|
/// 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
|
/// Despite the edge cases above, this will only ever remove the initial
|
||||||
/// an indefinite number of pages. In the most pathological cases (the screen
|
/// node, so that this can be called within a pageIterator. This is a weird
|
||||||
/// is one giant wrapped line), this can be a very expensive operation. That
|
/// detail that will surely cause bugs one day so we should look into fixing
|
||||||
/// doesn't really happen in typical terminal usage so its not a case we
|
/// it. :)
|
||||||
/// optimize for today. Contributions welcome to optimize this.
|
|
||||||
///
|
///
|
||||||
/// Conceptually, this is a simple process: we're effectively traversing
|
/// 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.
|
/// 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(
|
fn reflowPage(
|
||||||
self: *PageList,
|
self: *PageList,
|
||||||
cap: Capacity,
|
cap: Capacity,
|
||||||
node: *List.Node,
|
initial_node: *List.Node,
|
||||||
) !void {
|
) !void {
|
||||||
// The cursor tracks where we are in the source page.
|
// 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.
|
// This is used to count blank lines so that we don't copy those.
|
||||||
var blank_lines: usize = 0;
|
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
|
// Our new capacity when growing columns may also shrink rows. So we
|
||||||
// need to do a loop in order to potentially make multiple pages.
|
// 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.
|
// 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
|
// The new page always starts with a size of 1 because we know we have
|
||||||
// at least one row to copy from the src.
|
// at least one row to copy from the src.
|
||||||
@ -753,235 +787,264 @@ fn reflowPage(
|
|||||||
|
|
||||||
// Our new page goes before our src node. This will append it to any
|
// Our new page goes before our src node. This will append it to any
|
||||||
// previous pages we've created.
|
// previous pages we've created.
|
||||||
self.pages.insertBefore(node, dst_node);
|
self.pages.insertBefore(initial_node, dst_node);
|
||||||
|
|
||||||
// Continue traversing the source until we're out of space in our
|
src_loop: while (true) {
|
||||||
// destination or we've copied all our intended rows.
|
// Continue traversing the source until we're out of space in our
|
||||||
for (src_cursor.y..src_cursor.page.size.rows) |src_y| {
|
// destination or we've copied all our intended rows.
|
||||||
const prev_wrap = src_cursor.page_row.wrap;
|
const started_completing_wrap = src_completing_wrap;
|
||||||
src_cursor.cursorAbsolute(0, @intCast(src_y));
|
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;
|
||||||
|
|
||||||
// Trim trailing empty cells if the row is not wrapped. If the
|
const prev_wrap = src_cursor.page_row.wrap;
|
||||||
// row is wrapped then we don't trim trailing empty cells because
|
src_cursor.cursorAbsolute(0, @intCast(src_y));
|
||||||
// the empty cells can be meaningful.
|
|
||||||
const trailing_empty = src_cursor.countTrailingEmptyCells();
|
|
||||||
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 a tracked pin is in this row then we need to keep it
|
// Trim trailing empty cells if the row is not wrapped. If the
|
||||||
// even if it is empty, because it is somehow meaningful
|
// row is wrapped then we don't trim trailing empty cells because
|
||||||
// (usually the screen cursor), but we do trim the cells
|
// the empty cells can be meaningful.
|
||||||
// down to the desired size.
|
const trailing_empty = src_cursor.countTrailingEmptyCells();
|
||||||
//
|
const cols_len = cols_len: {
|
||||||
// The reason we do this logic is because if you do a scroll
|
var cols_len = src_cursor.page.size.cols - trailing_empty;
|
||||||
// clear (i.e. move all active into scrollback and reset
|
if (cols_len > 0) break :cols_len cols_len;
|
||||||
// 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
|
// If a tracked pin is in this row then we need to keep it
|
||||||
// trim it to the last col, we don't want to wrap blanks.
|
// even if it is empty, because it is somehow meaningful
|
||||||
if (p.x >= cap.cols) p.x = cap.cols - 1;
|
// (usually the screen cursor), but we do trim the cells
|
||||||
|
// down to the desired size.
|
||||||
|
//
|
||||||
|
// 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;
|
||||||
|
|
||||||
// We increase our col len to at least include this pin
|
// If our tracked pin is outside our resized cols, we
|
||||||
cols_len = @max(cols_len, p.x + 1);
|
// trim it to the last col, we don't want to wrap blanks.
|
||||||
}
|
if (p.x >= cap.cols) p.x = cap.cols - 1;
|
||||||
|
|
||||||
if (cols_len == 0) {
|
// We increase our col len to at least include this pin
|
||||||
// If the row is empty, we don't copy it. We count it as a
|
cols_len = @max(cols_len, p.x + 1);
|
||||||
// blank line and continue to the next row.
|
}
|
||||||
blank_lines += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
break :cols_len cols_len;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// We have data, if we have blank lines we need to create them first.
|
break :cols_len cols_len;
|
||||||
for (0..blank_lines) |_| {
|
};
|
||||||
// TODO: cursor in here
|
|
||||||
dst_cursor.cursorScroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (src_y > 0) {
|
// We have data, if we have blank lines we need to create them first.
|
||||||
// We're done with this row, if this row isn't wrapped, we can
|
for (0..blank_lines) |i| {
|
||||||
// move our destination cursor to the next row.
|
// If we're at the bottom we can't fit anymore into this page,
|
||||||
//
|
// so we need to reloop and create a new page.
|
||||||
// The blank_lines == 0 condition is because if we were prefixed
|
if (dst_cursor.bottom()) {
|
||||||
// with blank lines, we handled the scroll already above.
|
blank_lines -= i;
|
||||||
if (!prev_wrap and blank_lines == 0) {
|
continue :dst_loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: cursor in here
|
||||||
dst_cursor.cursorScroll();
|
dst_cursor.cursorScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
dst_cursor.copyRowMetadata(src_cursor.page_row);
|
if (src_y > 0) {
|
||||||
}
|
// We're done with this row, if this row isn't wrapped, we can
|
||||||
|
// move our destination cursor to the next row.
|
||||||
|
//
|
||||||
|
// The blank_lines == 0 condition is because if we were prefixed
|
||||||
|
// with blank lines, we handled the scroll already above.
|
||||||
|
if (!prev_wrap and blank_lines == 0) {
|
||||||
|
dst_cursor.cursorScroll();
|
||||||
|
}
|
||||||
|
|
||||||
// Reset our blank line count since handled it all above.
|
|
||||||
blank_lines = 0;
|
|
||||||
|
|
||||||
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}", .{
|
|
||||||
// src_cursor.y,
|
|
||||||
// src_cursor.x,
|
|
||||||
// dst_cursor.y,
|
|
||||||
// dst_cursor.x,
|
|
||||||
// src_cursor.page_cell.content.codepoint,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// If we have a wide char at the end of our page we need
|
|
||||||
// to insert a spacer head and wrap.
|
|
||||||
if (cap.cols > 1 and
|
|
||||||
src_cursor.page_cell.wide == .wide and
|
|
||||||
dst_cursor.x == cap.cols - 1)
|
|
||||||
{
|
|
||||||
self.reflowUpdateCursor(&src_cursor, &dst_cursor, dst_node);
|
|
||||||
|
|
||||||
dst_cursor.page_cell.* = .{
|
|
||||||
.content_tag = .codepoint,
|
|
||||||
.content = .{ .codepoint = 0 },
|
|
||||||
.wide = .spacer_head,
|
|
||||||
};
|
|
||||||
dst_cursor.cursorForward();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a spacer head and we're not at the end then
|
|
||||||
// we want to unwrap it and eliminate the head.
|
|
||||||
if (cap.cols > 1 and
|
|
||||||
src_cursor.page_cell.wide == .spacer_head and
|
|
||||||
dst_cursor.x != cap.cols - 1)
|
|
||||||
{
|
|
||||||
self.reflowUpdateCursor(&src_cursor, &dst_cursor, dst_node);
|
|
||||||
src_cursor.cursorForward();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dst_cursor.pending_wrap) {
|
|
||||||
dst_cursor.page_row.wrap = true;
|
|
||||||
dst_cursor.cursorScroll();
|
|
||||||
dst_cursor.page_row.wrap_continuation = true;
|
|
||||||
dst_cursor.copyRowMetadata(src_cursor.page_row);
|
dst_cursor.copyRowMetadata(src_cursor.page_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A rare edge case. If we're resizing down to 1 column
|
// Reset our blank line count since handled it all above.
|
||||||
// and the source is a non-narrow character, we reset the
|
blank_lines = 0;
|
||||||
// cell to a narrow blank and we skip to the next cell.
|
|
||||||
if (cap.cols == 1 and src_cursor.page_cell.wide != .narrow) {
|
|
||||||
switch (src_cursor.page_cell.wide) {
|
|
||||||
.narrow => unreachable,
|
|
||||||
|
|
||||||
// Wide char, we delete it, reset it to narrow,
|
for (src_cursor.x..cols_len) |src_x| {
|
||||||
// and skip forward.
|
assert(src_cursor.x == src_x);
|
||||||
.wide => {
|
|
||||||
dst_cursor.page_cell.content.codepoint = 0;
|
|
||||||
dst_cursor.page_cell.wide = .narrow;
|
|
||||||
src_cursor.cursorForward();
|
|
||||||
continue;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Skip spacer tails since we should've already
|
// std.log.warn("src_y={} src_x={} dst_y={} dst_x={} cp={}", .{
|
||||||
// handled them in the previous cell.
|
// src_cursor.y,
|
||||||
.spacer_tail => {},
|
// src_cursor.x,
|
||||||
|
// dst_cursor.y,
|
||||||
|
// dst_cursor.x,
|
||||||
|
// src_cursor.page_cell.content.codepoint,
|
||||||
|
// });
|
||||||
|
|
||||||
// TODO: test?
|
// If we have a wide char at the end of our page we need
|
||||||
.spacer_head => {},
|
// to insert a spacer head and wrap.
|
||||||
}
|
if (cap.cols > 1 and
|
||||||
} else {
|
src_cursor.page_cell.wide == .wide and
|
||||||
switch (src_cursor.page_cell.content_tag) {
|
dst_cursor.x == cap.cols - 1)
|
||||||
// These are guaranteed to have no styling data and no
|
{
|
||||||
// graphemes, a fast path.
|
self.reflowUpdateCursor(&src_cursor, &dst_cursor, dst_node);
|
||||||
.bg_color_palette,
|
|
||||||
.bg_color_rgb,
|
|
||||||
=> {
|
|
||||||
assert(!src_cursor.page_cell.hasStyling());
|
|
||||||
assert(!src_cursor.page_cell.hasGrapheme());
|
|
||||||
dst_cursor.page_cell.* = src_cursor.page_cell.*;
|
|
||||||
},
|
|
||||||
|
|
||||||
.codepoint => {
|
dst_cursor.page_cell.* = .{
|
||||||
dst_cursor.page_cell.* = src_cursor.page_cell.*;
|
.content_tag = .codepoint,
|
||||||
},
|
.content = .{ .codepoint = 0 },
|
||||||
|
.wide = .spacer_head,
|
||||||
.codepoint_grapheme => {
|
};
|
||||||
// We copy the cell like normal but we have to reset the
|
dst_cursor.cursorForward();
|
||||||
// tag because this is used for fast-path detection in
|
|
||||||
// appendGrapheme.
|
|
||||||
dst_cursor.page_cell.* = src_cursor.page_cell.*;
|
|
||||||
dst_cursor.page_cell.content_tag = .codepoint;
|
|
||||||
|
|
||||||
// Copy the graphemes
|
|
||||||
const src_cps = src_cursor.page.lookupGrapheme(src_cursor.page_cell).?;
|
|
||||||
for (src_cps) |cp| {
|
|
||||||
try dst_cursor.page.appendGrapheme(
|
|
||||||
dst_cursor.page_row,
|
|
||||||
dst_cursor.page_cell,
|
|
||||||
cp,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the source cell has a style, we need to copy it.
|
// If we have a spacer head and we're not at the end then
|
||||||
if (src_cursor.page_cell.style_id != stylepkg.default_id) {
|
// we want to unwrap it and eliminate the head.
|
||||||
const src_style = src_cursor.page.styles.lookupId(
|
if (cap.cols > 1 and
|
||||||
src_cursor.page.memory,
|
src_cursor.page_cell.wide == .spacer_head and
|
||||||
src_cursor.page_cell.style_id,
|
dst_cursor.x != cap.cols - 1)
|
||||||
).?.*;
|
{
|
||||||
|
self.reflowUpdateCursor(&src_cursor, &dst_cursor, dst_node);
|
||||||
const dst_md = try dst_cursor.page.styles.upsert(
|
src_cursor.cursorForward();
|
||||||
dst_cursor.page.memory,
|
continue;
|
||||||
src_style,
|
|
||||||
);
|
|
||||||
dst_md.ref += 1;
|
|
||||||
dst_cursor.page_cell.style_id = dst_md.id;
|
|
||||||
dst_cursor.page_row.styled = true;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If our original cursor was on this page, this x/y then
|
if (dst_cursor.pending_wrap) {
|
||||||
// we need to update to the new location.
|
dst_cursor.page_row.wrap = true;
|
||||||
self.reflowUpdateCursor(&src_cursor, &dst_cursor, dst_node);
|
dst_cursor.cursorScroll();
|
||||||
|
dst_cursor.page_row.wrap_continuation = true;
|
||||||
|
dst_cursor.copyRowMetadata(src_cursor.page_row);
|
||||||
|
}
|
||||||
|
|
||||||
// Move both our cursors forward
|
// A rare edge case. If we're resizing down to 1 column
|
||||||
src_cursor.cursorForward();
|
// and the source is a non-narrow character, we reset the
|
||||||
dst_cursor.cursorForward();
|
// cell to a narrow blank and we skip to the next cell.
|
||||||
} else cursor: {
|
if (cap.cols == 1 and src_cursor.page_cell.wide != .narrow) {
|
||||||
// We made it through all our source columns. As a final edge
|
switch (src_cursor.page_cell.wide) {
|
||||||
// case, if our cursor is in one of the blanks, we update it
|
.narrow => unreachable,
|
||||||
// to the edge of this page.
|
|
||||||
|
|
||||||
// If we have no trailing empty cells, it can't be in the blanks.
|
// Wide char, we delete it, reset it to narrow,
|
||||||
if (trailing_empty == 0) break :cursor;
|
// and skip forward.
|
||||||
|
.wide => {
|
||||||
|
dst_cursor.page_cell.content.codepoint = 0;
|
||||||
|
dst_cursor.page_cell.wide = .narrow;
|
||||||
|
src_cursor.cursorForward();
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
|
||||||
// Update all our tracked pins
|
// Skip spacer tails since we should've already
|
||||||
var it = self.tracked_pins.keyIterator();
|
// handled them in the previous cell.
|
||||||
while (it.next()) |p_ptr| {
|
.spacer_tail => {},
|
||||||
const p = p_ptr.*;
|
|
||||||
if (&p.page.data != src_cursor.page or
|
|
||||||
p.y != src_cursor.y or
|
|
||||||
p.x < cols_len) continue;
|
|
||||||
|
|
||||||
p.page = dst_node;
|
// TODO: test?
|
||||||
p.y = dst_cursor.y;
|
.spacer_head => {},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (src_cursor.page_cell.content_tag) {
|
||||||
|
// These are guaranteed to have no styling data and no
|
||||||
|
// graphemes, a fast path.
|
||||||
|
.bg_color_palette,
|
||||||
|
.bg_color_rgb,
|
||||||
|
=> {
|
||||||
|
assert(!src_cursor.page_cell.hasStyling());
|
||||||
|
assert(!src_cursor.page_cell.hasGrapheme());
|
||||||
|
dst_cursor.page_cell.* = src_cursor.page_cell.*;
|
||||||
|
},
|
||||||
|
|
||||||
|
.codepoint => {
|
||||||
|
dst_cursor.page_cell.* = src_cursor.page_cell.*;
|
||||||
|
},
|
||||||
|
|
||||||
|
.codepoint_grapheme => {
|
||||||
|
// We copy the cell like normal but we have to reset the
|
||||||
|
// tag because this is used for fast-path detection in
|
||||||
|
// appendGrapheme.
|
||||||
|
dst_cursor.page_cell.* = src_cursor.page_cell.*;
|
||||||
|
dst_cursor.page_cell.content_tag = .codepoint;
|
||||||
|
|
||||||
|
// Copy the graphemes
|
||||||
|
const src_cps = src_cursor.page.lookupGrapheme(src_cursor.page_cell).?;
|
||||||
|
for (src_cps) |cp| {
|
||||||
|
try dst_cursor.page.appendGrapheme(
|
||||||
|
dst_cursor.page_row,
|
||||||
|
dst_cursor.page_cell,
|
||||||
|
cp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the source cell has a style, we need to copy it.
|
||||||
|
if (src_cursor.page_cell.style_id != stylepkg.default_id) {
|
||||||
|
const src_style = src_cursor.page.styles.lookupId(
|
||||||
|
src_cursor.page.memory,
|
||||||
|
src_cursor.page_cell.style_id,
|
||||||
|
).?.*;
|
||||||
|
|
||||||
|
const dst_md = try dst_cursor.page.styles.upsert(
|
||||||
|
dst_cursor.page.memory,
|
||||||
|
src_style,
|
||||||
|
);
|
||||||
|
dst_md.ref += 1;
|
||||||
|
dst_cursor.page_cell.style_id = dst_md.id;
|
||||||
|
dst_cursor.page_row.styled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our original cursor was on this page, this x/y then
|
||||||
|
// we need to update to the new location.
|
||||||
|
self.reflowUpdateCursor(&src_cursor, &dst_cursor, dst_node);
|
||||||
|
|
||||||
|
// Move both our cursors forward
|
||||||
|
src_cursor.cursorForward();
|
||||||
|
dst_cursor.cursorForward();
|
||||||
|
} else cursor: {
|
||||||
|
// We made it through all our source columns. As a final edge
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Update all our tracked pins
|
||||||
|
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 or
|
||||||
|
p.x < cols_len) continue;
|
||||||
|
|
||||||
|
p.page = dst_node;
|
||||||
|
p.y = dst_cursor.y;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// We made it through all our source rows, we're done.
|
// If we're still in a wrapped line at the end of our page,
|
||||||
break;
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, remove the old page.
|
|
||||||
self.pages.remove(node);
|
|
||||||
self.destroyPage(node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This updates the cursor offset if the cursor is exactly on the cell
|
/// 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" {
|
test "PageList resize reflow more cols cursor in wrapped row" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
Reference in New Issue
Block a user