terminal: preserve aliased selection pins

Screen.select accepted an already tracked selection by value. Passing the screen's current selection back into the setter caused the old selection cleanup to free the same pin pair that the replacement retained, so the next selection operation dereferenced stale pool entries and panicked.

When replacing tracked state, release only old pins that are not also owned by the replacement. Exact and partial aliases now retain their shared pins while ordinary replacements still reclaim both old entries.
This commit is contained in:
Mitchell Hashimoto
2026-07-09 21:50:33 -07:00
parent 91f0cf67d5
commit 0c299000f8

View File

@@ -2624,8 +2624,28 @@ pub fn select(self: *Screen, sel_: ?Selection) Allocator.Error!void {
const tracked_sel = if (sel.tracked()) sel else try sel.track(self);
errdefer if (!sel.tracked()) tracked_sel.deinit(self);
// Untrack prior selection
if (self.selection) |*old| old.deinit(self);
// Untrack prior selection pins that aren't also owned by the replacement.
// A caller may pass our current tracked selection back to us by value, so
// releasing both old pins unconditionally would leave the replacement
// pointing at freed pool entries.
if (self.selection) |old| {
const new_bounds = tracked_sel.bounds.tracked;
switch (old.bounds) {
.untracked => old.deinit(self),
.tracked => |old_bounds| {
if (old_bounds.start != new_bounds.start and
old_bounds.start != new_bounds.end)
{
self.pages.untrackPin(old_bounds.start);
}
if (old_bounds.end != new_bounds.start and
old_bounds.end != new_bounds.end)
{
self.pages.untrackPin(old_bounds.end);
}
},
}
}
self.selection = tracked_sel;
self.dirty.selection = true;
}
@@ -8224,6 +8244,23 @@ test "Screen: select replaces existing pins" {
try testing.expectEqual(tracked + 2, s.pages.countTrackedPins());
}
test "Screen: reselecting tracked selection preserves its pins" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, .{ .cols = 10, .rows = 2, .max_scrollback = 0 });
defer s.deinit();
try s.select(Selection.init(
s.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
s.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
false,
));
try s.select(s.selection.?);
try testing.expectEqual(Selection.Order.forward, s.selection.?.order(&s));
}
test "Screen: selectAll" {
const testing = std.testing;
const alloc = testing.allocator;