mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-19 14:00:29 +00:00
terminal: PageList split operation
This commit is contained in:
@@ -2753,6 +2753,98 @@ pub fn compact(self: *PageList, node: *List.Node) Allocator.Error!?*List.Node {
|
||||
new_page.assertIntegrity();
|
||||
return new_node;
|
||||
}
|
||||
|
||||
pub const SplitError = error{
|
||||
// Allocator OOM
|
||||
OutOfMemory,
|
||||
// Page can't be split further because it is already a single row.
|
||||
OutOfSpace,
|
||||
};
|
||||
|
||||
/// Split the given node in the PageList at the given pin.
|
||||
///
|
||||
/// The row at the pin and after will be moved into a new page with
|
||||
/// the same capacity as the original page. Alternatively, you can "split
|
||||
/// above" by splitting the row following the desired split row.
|
||||
///
|
||||
/// Since the split happens below the pin, the pin remains valid.
|
||||
pub fn split(
|
||||
self: *PageList,
|
||||
p: Pin,
|
||||
) SplitError!void {
|
||||
if (build_options.slow_runtime_safety) assert(self.pinIsValid(p));
|
||||
|
||||
// Ran into a bug that I can only explain via aliasing. If a tracked
|
||||
// pin is passed in, its possible Zig will alias the memory and then
|
||||
// when we modify it later it updates our p here. Coyping the node
|
||||
// fixes this.
|
||||
const original_node = p.node;
|
||||
const page: *Page = &original_node.data;
|
||||
|
||||
// A page that is already 1 row can't be split. In the future we can
|
||||
// theoretically maybe split by soft-wrapping multiple pages but that
|
||||
// seems crazy and the rest of our PageList can't handle heterogeneously
|
||||
// sized pages today.
|
||||
if (page.size.rows <= 1) return error.OutOfSpace;
|
||||
|
||||
// Splitting at row 0 is a no-op since there's nothing before the split point.
|
||||
if (p.y == 0) return;
|
||||
|
||||
// At this point we're doing actual modification so make sure
|
||||
// on the return that we're good.
|
||||
defer self.assertIntegrity();
|
||||
|
||||
// Create a new node with the same capacity of managed memory.
|
||||
const target = try self.createPage(page.capacity);
|
||||
errdefer self.destroyNode(target);
|
||||
|
||||
// Determine how many rows we're copying
|
||||
const y_start = p.y;
|
||||
const y_end = page.size.rows;
|
||||
target.data.size.rows = y_end - y_start;
|
||||
assert(target.data.size.rows <= target.data.capacity.rows);
|
||||
|
||||
// Copy our old data. This should NOT fail because we have the
|
||||
// capacity of the old page which already fits the data we requested.
|
||||
target.data.cloneFrom(page, y_start, y_end) catch |err| {
|
||||
log.err(
|
||||
"error cloning rows for split err={}",
|
||||
.{err},
|
||||
);
|
||||
|
||||
// Rather than crash, we return an OutOfSpace to show that
|
||||
// we couldn't split and let our callers gracefully handle it.
|
||||
// Realistically though... this should not happen.
|
||||
return error.OutOfSpace;
|
||||
};
|
||||
|
||||
// From this point forward there is no going back. We have no
|
||||
// error handling. It is possible but we haven't written it.
|
||||
errdefer comptime unreachable;
|
||||
|
||||
// Move any tracked pins from the copied rows
|
||||
for (self.tracked_pins.keys()) |tracked| {
|
||||
if (&tracked.node.data != page or
|
||||
tracked.y < p.y) continue;
|
||||
|
||||
tracked.node = target;
|
||||
tracked.y -= p.y;
|
||||
// p.x remains the same since we're copying the row as-is
|
||||
}
|
||||
|
||||
// Clear our rows
|
||||
for (page.rows.ptr(page.memory)[y_start..y_end]) |*row| {
|
||||
page.clearCells(
|
||||
row,
|
||||
0,
|
||||
page.size.cols,
|
||||
);
|
||||
}
|
||||
page.size.rows -= y_end - y_start;
|
||||
|
||||
self.pages.insertAfter(original_node, target);
|
||||
}
|
||||
|
||||
/// This represents the state necessary to render a scrollbar for this
|
||||
/// PageList. It has the total size, the offset, and the size of the viewport.
|
||||
pub const Scrollbar = struct {
|
||||
@@ -12013,3 +12105,596 @@ test "PageList compact insufficient savings returns null" {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList split at middle row" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const page = &s.pages.first.?.data;
|
||||
|
||||
// Write content to rows: row 0 gets codepoint 0, row 1 gets 1, etc.
|
||||
for (0..page.size.rows) |y| {
|
||||
const rac = page.getRowAndCell(0, y);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = @intCast(y) },
|
||||
};
|
||||
}
|
||||
|
||||
// Split at row 5 (middle)
|
||||
const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
// Verify two pages exist
|
||||
try testing.expect(s.pages.first != null);
|
||||
try testing.expect(s.pages.first.?.next != null);
|
||||
|
||||
const first_page = &s.pages.first.?.data;
|
||||
const second_page = &s.pages.first.?.next.?.data;
|
||||
|
||||
// First page should have rows 0-4 (5 rows)
|
||||
try testing.expectEqual(@as(usize, 5), first_page.size.rows);
|
||||
// Second page should have rows 5-9 (5 rows)
|
||||
try testing.expectEqual(@as(usize, 5), second_page.size.rows);
|
||||
|
||||
// Verify content in first page is preserved (rows 0-4 have codepoints 0-4)
|
||||
for (0..5) |y| {
|
||||
const rac = first_page.getRowAndCell(0, y);
|
||||
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
|
||||
}
|
||||
|
||||
// Verify content in second page (original rows 5-9, now at y=0-4)
|
||||
for (0..5) |y| {
|
||||
const rac = second_page.getRowAndCell(0, y);
|
||||
try testing.expectEqual(@as(u21, @intCast(y + 5)), rac.cell.content.codepoint);
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList split at row 0 is no-op" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const page = &s.pages.first.?.data;
|
||||
|
||||
// Write content to all rows
|
||||
for (0..page.size.rows) |y| {
|
||||
const rac = page.getRowAndCell(0, y);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = @intCast(y) },
|
||||
};
|
||||
}
|
||||
|
||||
// Split at row 0 should be a no-op
|
||||
const split_pin: Pin = .{ .node = s.pages.first.?, .y = 0, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
// Verify only one page exists (no split occurred)
|
||||
try testing.expect(s.pages.first != null);
|
||||
try testing.expect(s.pages.first.?.next == null);
|
||||
|
||||
// Verify all content is still in the original page
|
||||
try testing.expectEqual(@as(usize, 10), page.size.rows);
|
||||
for (0..10) |y| {
|
||||
const rac = page.getRowAndCell(0, y);
|
||||
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList split at last row" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const page = &s.pages.first.?.data;
|
||||
|
||||
// Write content to all rows
|
||||
for (0..page.size.rows) |y| {
|
||||
const rac = page.getRowAndCell(0, y);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = @intCast(y) },
|
||||
};
|
||||
}
|
||||
|
||||
// Split at last row (row 9)
|
||||
const split_pin: Pin = .{ .node = s.pages.first.?, .y = 9, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
// Verify two pages exist
|
||||
try testing.expect(s.pages.first != null);
|
||||
try testing.expect(s.pages.first.?.next != null);
|
||||
|
||||
const first_page = &s.pages.first.?.data;
|
||||
const second_page = &s.pages.first.?.next.?.data;
|
||||
|
||||
// First page should have 9 rows
|
||||
try testing.expectEqual(@as(usize, 9), first_page.size.rows);
|
||||
// Second page should have 1 row
|
||||
try testing.expectEqual(@as(usize, 1), second_page.size.rows);
|
||||
|
||||
// Verify content in second page (original row 9, now at y=0)
|
||||
const rac = second_page.getRowAndCell(0, 0);
|
||||
try testing.expectEqual(@as(u21, 9), rac.cell.content.codepoint);
|
||||
}
|
||||
|
||||
test "PageList split single row page returns OutOfSpace" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Initialize with 1 row
|
||||
var s = try init(alloc, 10, 1, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const split_pin: Pin = .{ .node = s.pages.first.?, .y = 0, .x = 0 };
|
||||
const result = s.split(split_pin);
|
||||
|
||||
try testing.expectError(error.OutOfSpace, result);
|
||||
}
|
||||
|
||||
test "PageList split moves tracked pins" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
// Track a pin at row 7
|
||||
const tracked = try s.trackPin(.{ .node = s.pages.first.?, .y = 7, .x = 3 });
|
||||
defer s.untrackPin(tracked);
|
||||
|
||||
// Split at row 5
|
||||
const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
// The tracked pin should now be in the second page
|
||||
try testing.expect(tracked.node == s.pages.first.?.next.?);
|
||||
// y should be adjusted: was 7, split at 5, so new y = 7 - 5 = 2
|
||||
try testing.expectEqual(@as(usize, 2), tracked.y);
|
||||
// x should remain unchanged
|
||||
try testing.expectEqual(@as(usize, 3), tracked.x);
|
||||
}
|
||||
|
||||
test "PageList split tracked pin before split point unchanged" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const original_node = s.pages.first.?;
|
||||
|
||||
// Track a pin at row 2 (before the split point)
|
||||
const tracked = try s.trackPin(.{ .node = original_node, .y = 2, .x = 5 });
|
||||
defer s.untrackPin(tracked);
|
||||
|
||||
// Split at row 5
|
||||
const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
// The tracked pin should remain in the original page
|
||||
try testing.expect(tracked.node == s.pages.first.?);
|
||||
// y and x should be unchanged
|
||||
try testing.expectEqual(@as(usize, 2), tracked.y);
|
||||
try testing.expectEqual(@as(usize, 5), tracked.x);
|
||||
}
|
||||
|
||||
test "PageList split tracked pin at split point moves to new page" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const original_node = s.pages.first.?;
|
||||
|
||||
// Track a pin at the exact split point (row 5)
|
||||
const tracked = try s.trackPin(.{ .node = original_node, .y = 5, .x = 4 });
|
||||
defer s.untrackPin(tracked);
|
||||
|
||||
// Split at row 5
|
||||
const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
// The tracked pin should be in the new page
|
||||
try testing.expect(tracked.node == s.pages.first.?.next.?);
|
||||
// y should be 0 since it was at the split point: 5 - 5 = 0
|
||||
try testing.expectEqual(@as(usize, 0), tracked.y);
|
||||
// x should remain unchanged
|
||||
try testing.expectEqual(@as(usize, 4), tracked.x);
|
||||
}
|
||||
|
||||
test "PageList split multiple tracked pins across regions" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const original_node = s.pages.first.?;
|
||||
|
||||
// Track multiple pins in different regions
|
||||
const pin_before = try s.trackPin(.{ .node = original_node, .y = 1, .x = 0 });
|
||||
defer s.untrackPin(pin_before);
|
||||
const pin_at_split = try s.trackPin(.{ .node = original_node, .y = 5, .x = 2 });
|
||||
defer s.untrackPin(pin_at_split);
|
||||
const pin_after1 = try s.trackPin(.{ .node = original_node, .y = 7, .x = 3 });
|
||||
defer s.untrackPin(pin_after1);
|
||||
const pin_after2 = try s.trackPin(.{ .node = original_node, .y = 9, .x = 8 });
|
||||
defer s.untrackPin(pin_after2);
|
||||
|
||||
// Split at row 5
|
||||
const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
const first_page = s.pages.first.?;
|
||||
const second_page = first_page.next.?;
|
||||
|
||||
// Pin before split point stays in original page
|
||||
try testing.expect(pin_before.node == first_page);
|
||||
try testing.expectEqual(@as(usize, 1), pin_before.y);
|
||||
try testing.expectEqual(@as(usize, 0), pin_before.x);
|
||||
|
||||
// Pin at split point moves to new page with y=0
|
||||
try testing.expect(pin_at_split.node == second_page);
|
||||
try testing.expectEqual(@as(usize, 0), pin_at_split.y);
|
||||
try testing.expectEqual(@as(usize, 2), pin_at_split.x);
|
||||
|
||||
// Pins after split point move to new page with adjusted y
|
||||
try testing.expect(pin_after1.node == second_page);
|
||||
try testing.expectEqual(@as(usize, 2), pin_after1.y); // 7 - 5 = 2
|
||||
try testing.expectEqual(@as(usize, 3), pin_after1.x);
|
||||
|
||||
try testing.expect(pin_after2.node == second_page);
|
||||
try testing.expectEqual(@as(usize, 4), pin_after2.y); // 9 - 5 = 4
|
||||
try testing.expectEqual(@as(usize, 8), pin_after2.x);
|
||||
}
|
||||
|
||||
test "PageList split tracked viewport_pin in split region moves correctly" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const original_node = s.pages.first.?;
|
||||
|
||||
// Set viewport_pin to row 7 (after split point)
|
||||
s.viewport_pin.node = original_node;
|
||||
s.viewport_pin.y = 7;
|
||||
s.viewport_pin.x = 6;
|
||||
|
||||
// Split at row 5
|
||||
const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
// viewport_pin should be in the new page
|
||||
try testing.expect(s.viewport_pin.node == s.pages.first.?.next.?);
|
||||
// y should be adjusted: 7 - 5 = 2
|
||||
try testing.expectEqual(@as(usize, 2), s.viewport_pin.y);
|
||||
// x should remain unchanged
|
||||
try testing.expectEqual(@as(usize, 6), s.viewport_pin.x);
|
||||
}
|
||||
|
||||
test "PageList split middle page preserves linked list order" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Create a single page with 12 rows
|
||||
var s = try init(alloc, 10, 12, 0);
|
||||
defer s.deinit();
|
||||
|
||||
// Split at row 4 to create: page1 (rows 0-3), page2 (rows 4-11)
|
||||
const first_node = s.pages.first.?;
|
||||
const split_pin1: Pin = .{ .node = first_node, .y = 4, .x = 0 };
|
||||
try s.split(split_pin1);
|
||||
|
||||
// Now we have 2 pages
|
||||
const page1 = s.pages.first.?;
|
||||
const page2 = s.pages.first.?.next.?;
|
||||
try testing.expectEqual(@as(usize, 4), page1.data.size.rows);
|
||||
try testing.expectEqual(@as(usize, 8), page2.data.size.rows);
|
||||
|
||||
// Split page2 at row 4 to create: page1 -> page2 (rows 0-3) -> page3 (rows 4-7)
|
||||
const split_pin2: Pin = .{ .node = page2, .y = 4, .x = 0 };
|
||||
try s.split(split_pin2);
|
||||
|
||||
// Now we have 3 pages
|
||||
const first = s.pages.first.?;
|
||||
const middle = first.next.?;
|
||||
const last = middle.next.?;
|
||||
|
||||
// Verify linked list order: first -> middle -> last
|
||||
try testing.expectEqual(page1, first);
|
||||
try testing.expectEqual(page2, middle);
|
||||
try testing.expectEqual(s.pages.last.?, last);
|
||||
|
||||
// Verify prev pointers
|
||||
try testing.expect(first.prev == null);
|
||||
try testing.expectEqual(first, middle.prev.?);
|
||||
try testing.expectEqual(middle, last.prev.?);
|
||||
|
||||
// Verify next pointers
|
||||
try testing.expectEqual(middle, first.next.?);
|
||||
try testing.expectEqual(last, middle.next.?);
|
||||
try testing.expect(last.next == null);
|
||||
|
||||
// Verify row counts
|
||||
try testing.expectEqual(@as(usize, 4), first.data.size.rows);
|
||||
try testing.expectEqual(@as(usize, 4), middle.data.size.rows);
|
||||
try testing.expectEqual(@as(usize, 4), last.data.size.rows);
|
||||
}
|
||||
|
||||
test "PageList split last page makes new page the last" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Create a single page with 10 rows
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
// Split to create 2 pages first
|
||||
const first_node = s.pages.first.?;
|
||||
const split_pin1: Pin = .{ .node = first_node, .y = 5, .x = 0 };
|
||||
try s.split(split_pin1);
|
||||
|
||||
// Now split the last page
|
||||
const last_before_split = s.pages.last.?;
|
||||
try testing.expectEqual(@as(usize, 5), last_before_split.data.size.rows);
|
||||
|
||||
const split_pin2: Pin = .{ .node = last_before_split, .y = 2, .x = 0 };
|
||||
try s.split(split_pin2);
|
||||
|
||||
// The new page should be the new last
|
||||
const new_last = s.pages.last.?;
|
||||
try testing.expect(new_last != last_before_split);
|
||||
try testing.expectEqual(last_before_split, new_last.prev.?);
|
||||
try testing.expect(new_last.next == null);
|
||||
|
||||
// Verify row counts: original last has 2 rows, new last has 3 rows
|
||||
try testing.expectEqual(@as(usize, 2), last_before_split.data.size.rows);
|
||||
try testing.expectEqual(@as(usize, 3), new_last.data.size.rows);
|
||||
}
|
||||
|
||||
test "PageList split first page keeps original as first" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Create 2 pages by splitting
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const original_first = s.pages.first.?;
|
||||
const split_pin1: Pin = .{ .node = original_first, .y = 5, .x = 0 };
|
||||
try s.split(split_pin1);
|
||||
|
||||
// Get second page (created by first split)
|
||||
const second_page = s.pages.first.?.next.?;
|
||||
|
||||
// Now split the first page again
|
||||
const split_pin2: Pin = .{ .node = s.pages.first.?, .y = 2, .x = 0 };
|
||||
try s.split(split_pin2);
|
||||
|
||||
// Original first should still be first
|
||||
try testing.expectEqual(original_first, s.pages.first.?);
|
||||
try testing.expect(s.pages.first.?.prev == null);
|
||||
|
||||
// New page should be inserted between first and second
|
||||
const inserted = s.pages.first.?.next.?;
|
||||
try testing.expect(inserted != second_page);
|
||||
try testing.expectEqual(second_page, inserted.next.?);
|
||||
|
||||
// Verify row counts: first has 2, inserted has 3, second has 5
|
||||
try testing.expectEqual(@as(usize, 2), s.pages.first.?.data.size.rows);
|
||||
try testing.expectEqual(@as(usize, 3), inserted.data.size.rows);
|
||||
try testing.expectEqual(@as(usize, 5), second_page.data.size.rows);
|
||||
}
|
||||
|
||||
test "PageList split preserves wrap flags" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const page = &s.pages.first.?.data;
|
||||
|
||||
// Set wrap flags on rows that will be in the second page after split
|
||||
// Row 5: wrap = true (this is the start of a wrapped line)
|
||||
// Row 6: wrap_continuation = true (this continues the wrap)
|
||||
// Row 7: wrap = true, wrap_continuation = true (wrapped and continues)
|
||||
{
|
||||
const rac5 = page.getRowAndCell(0, 5);
|
||||
rac5.row.wrap = true;
|
||||
|
||||
const rac6 = page.getRowAndCell(0, 6);
|
||||
rac6.row.wrap_continuation = true;
|
||||
|
||||
const rac7 = page.getRowAndCell(0, 7);
|
||||
rac7.row.wrap = true;
|
||||
rac7.row.wrap_continuation = true;
|
||||
}
|
||||
|
||||
// Split at row 5
|
||||
const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
const second_page = &s.pages.first.?.next.?.data;
|
||||
|
||||
// Verify wrap flags are preserved in new page
|
||||
// Original row 5 is now row 0 in second page
|
||||
{
|
||||
const rac0 = second_page.getRowAndCell(0, 0);
|
||||
try testing.expect(rac0.row.wrap);
|
||||
try testing.expect(!rac0.row.wrap_continuation);
|
||||
}
|
||||
|
||||
// Original row 6 is now row 1 in second page
|
||||
{
|
||||
const rac1 = second_page.getRowAndCell(0, 1);
|
||||
try testing.expect(!rac1.row.wrap);
|
||||
try testing.expect(rac1.row.wrap_continuation);
|
||||
}
|
||||
|
||||
// Original row 7 is now row 2 in second page
|
||||
{
|
||||
const rac2 = second_page.getRowAndCell(0, 2);
|
||||
try testing.expect(rac2.row.wrap);
|
||||
try testing.expect(rac2.row.wrap_continuation);
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList split preserves styled cells" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const page = &s.pages.first.?.data;
|
||||
|
||||
// Create a style and apply it to cells in rows 5-7 (which will be in the second page)
|
||||
const style: stylepkg.Style = .{ .flags = .{ .bold = true } };
|
||||
const style_id = try page.styles.add(page.memory, style);
|
||||
|
||||
for (5..8) |y| {
|
||||
const rac = page.getRowAndCell(0, y);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 'S' },
|
||||
.style_id = style_id,
|
||||
};
|
||||
rac.row.styled = true;
|
||||
page.styles.use(page.memory, style_id);
|
||||
}
|
||||
// Release the extra ref from add
|
||||
page.styles.release(page.memory, style_id);
|
||||
|
||||
// Split at row 5
|
||||
const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
const first_page = &s.pages.first.?.data;
|
||||
const second_page = &s.pages.first.?.next.?.data;
|
||||
|
||||
// First page should have no styles (all styled rows moved to second page)
|
||||
try testing.expectEqual(@as(usize, 0), first_page.styles.count());
|
||||
|
||||
// Second page should have exactly 1 style (the bold style, used by 3 cells)
|
||||
try testing.expectEqual(@as(usize, 1), second_page.styles.count());
|
||||
|
||||
// Verify styled cells are preserved in new page
|
||||
for (0..3) |y| {
|
||||
const rac = second_page.getRowAndCell(0, y);
|
||||
try testing.expectEqual(@as(u21, 'S'), rac.cell.content.codepoint);
|
||||
try testing.expect(rac.cell.style_id != 0);
|
||||
|
||||
const got_style = second_page.styles.get(second_page.memory, rac.cell.style_id);
|
||||
try testing.expect(got_style.flags.bold);
|
||||
try testing.expect(rac.row.styled);
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList split preserves grapheme clusters" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const page = &s.pages.first.?.data;
|
||||
|
||||
// Add a grapheme cluster to row 6 (will be row 1 in second page after split at 5)
|
||||
{
|
||||
const rac = page.getRowAndCell(0, 6);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 0x1F468 }, // Man emoji
|
||||
};
|
||||
try page.setGraphemes(rac.row, rac.cell, &.{
|
||||
0x200D, // ZWJ
|
||||
0x1F469, // Woman emoji
|
||||
});
|
||||
}
|
||||
|
||||
// Split at row 5
|
||||
const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
const first_page = &s.pages.first.?.data;
|
||||
const second_page = &s.pages.first.?.next.?.data;
|
||||
|
||||
// First page should have no graphemes (the grapheme row moved to second page)
|
||||
try testing.expectEqual(@as(usize, 0), first_page.graphemeCount());
|
||||
|
||||
// Second page should have exactly 1 grapheme
|
||||
try testing.expectEqual(@as(usize, 1), second_page.graphemeCount());
|
||||
|
||||
// Verify grapheme is preserved in new page (original row 6 is now row 1)
|
||||
{
|
||||
const rac = second_page.getRowAndCell(0, 1);
|
||||
try testing.expectEqual(@as(u21, 0x1F468), rac.cell.content.codepoint);
|
||||
try testing.expect(rac.row.grapheme);
|
||||
|
||||
const cps = second_page.lookupGrapheme(rac.cell).?;
|
||||
try testing.expectEqual(@as(usize, 2), cps.len);
|
||||
try testing.expectEqual(@as(u21, 0x200D), cps[0]);
|
||||
try testing.expectEqual(@as(u21, 0x1F469), cps[1]);
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList split preserves hyperlinks" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
|
||||
const page = &s.pages.first.?.data;
|
||||
|
||||
// Add a hyperlink to row 7 (will be row 2 in second page after split at 5)
|
||||
const hyperlink_id = try page.insertHyperlink(.{
|
||||
.id = .{ .implicit = 0 },
|
||||
.uri = "https://example.com",
|
||||
});
|
||||
{
|
||||
const rac = page.getRowAndCell(0, 7);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 'L' },
|
||||
};
|
||||
try page.setHyperlink(rac.row, rac.cell, hyperlink_id);
|
||||
}
|
||||
|
||||
// Split at row 5
|
||||
const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 };
|
||||
try s.split(split_pin);
|
||||
|
||||
const first_page = &s.pages.first.?.data;
|
||||
const second_page = &s.pages.first.?.next.?.data;
|
||||
|
||||
// First page should have no hyperlinks (the hyperlink row moved to second page)
|
||||
try testing.expectEqual(@as(usize, 0), first_page.hyperlink_set.count());
|
||||
|
||||
// Second page should have exactly 1 hyperlink
|
||||
try testing.expectEqual(@as(usize, 1), second_page.hyperlink_set.count());
|
||||
|
||||
// Verify hyperlink is preserved in new page (original row 7 is now row 2)
|
||||
{
|
||||
const rac = second_page.getRowAndCell(0, 2);
|
||||
try testing.expectEqual(@as(u21, 'L'), rac.cell.content.codepoint);
|
||||
try testing.expect(rac.cell.hyperlink);
|
||||
|
||||
const link_id = second_page.lookupHyperlink(rac.cell).?;
|
||||
const link = second_page.hyperlink_set.get(second_page.memory, link_id);
|
||||
try testing.expectEqualStrings("https://example.com", link.uri.slice(second_page.memory));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user