terminal: clamp cloned selections to page width

Screen.clone clipped selections with the desired screen width or copied
rectangle columns unchanged. PageList.clone preserves stored page widths,
so a clipped boundary on a narrower page produced an invalid tracked pin
and panicked during runtime validation.

Build fallback pins from the first or last cloned node and clamp rectangle
columns to that node. Clipped selections now remain valid during reflow.
This commit is contained in:
Mitchell Hashimoto
2026-07-09 21:03:28 -07:00
parent 08e376f239
commit 6071606577

View File

@@ -530,20 +530,30 @@ pub fn clone(
}
// We move the top pin back in bounds to the top row.
const node = pages.pages.first.?;
break :start try pages.trackPin(.{
.node = pages.pages.first.?,
.x = if (sel.rectangle) ordered.tl.x else 0,
.node = node,
.x = if (sel.rectangle)
@min(ordered.tl.x, node.cols() - 1)
else
0,
});
};
// If we got to this point it means that the selection is not
// fully out of bounds, so we move the bottom right pin back
// in bounds if it isn't already.
const end_pin = pin_remap.get(ordered.br) orelse try pages.trackPin(.{
.node = pages.pages.last.?,
.x = if (sel.rectangle) ordered.br.x else pages.cols - 1,
.y = pages.pages.last.?.rows() - 1,
});
const end_pin = pin_remap.get(ordered.br) orelse end: {
const node = pages.pages.last.?;
break :end try pages.trackPin(.{
.node = node,
.x = if (sel.rectangle)
@min(ordered.br.x, node.cols() - 1)
else
node.cols() - 1,
.y = node.rows() - 1,
});
};
break :sel .{
.bounds = .{ .tracked = .{
@@ -5969,6 +5979,51 @@ test "Screen: clone contains subset of selection" {
}
}
test "Screen: clone clamps clipped selections to mixed-width pages" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, .{ .cols = 4, .rows = 3, .max_scrollback = 0 });
defer s.deinit();
const first = s.pages.pages.first.?;
try s.pages.split(.{ .node = first, .y = 2 });
try s.pages.split(.{ .node = first, .y = 1 });
const middle = first.next.?;
const last = middle.next.?;
middle.page().size.cols = 2;
try s.select(Selection.init(
.{ .node = first },
.{ .node = last, .x = 3 },
false,
));
var linear = try s.clone(
alloc,
.{ .screen = .{} },
.{ .screen = .{ .y = 1 } },
);
defer linear.deinit();
const linear_end = linear.selection.?.end();
_ = linear_end.rowAndCell();
try testing.expectEqual(@as(size.CellCountInt, 1), linear_end.x);
try s.select(Selection.init(
.{ .node = first, .x = 3 },
.{ .node = last, .x = 3 },
true,
));
var rectangle = try s.clone(
alloc,
.{ .screen = .{ .y = 1 } },
null,
);
defer rectangle.deinit();
const rectangle_start = rectangle.selection.?.start();
_ = rectangle_start.rowAndCell();
try testing.expectEqual(@as(size.CellCountInt, 1), rectangle_start.x);
}
test "Screen: clone contains subset of rectangle selection" {
const testing = std.testing;
const alloc = testing.allocator;