mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
terminal2: working on pins and tracked pins
This commit is contained in:
@ -43,11 +43,18 @@ const PagePool = std.heap.MemoryPoolAligned(
|
|||||||
std.mem.page_size,
|
std.mem.page_size,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// List of pins, known as "tracked" pins. These are pins that are kept
|
||||||
|
/// up to date automatically through page-modifying operations.
|
||||||
|
const PinSet = std.AutoHashMapUnmanaged(*Pin, void);
|
||||||
|
const PinPool = std.heap.MemoryPool(Pin);
|
||||||
|
|
||||||
/// The pool of memory used for a pagelist. This can be shared between
|
/// The pool of memory used for a pagelist. This can be shared between
|
||||||
/// multiple pagelists but it is not threadsafe.
|
/// multiple pagelists but it is not threadsafe.
|
||||||
pub const MemoryPool = struct {
|
pub const MemoryPool = struct {
|
||||||
|
alloc: Allocator,
|
||||||
nodes: NodePool,
|
nodes: NodePool,
|
||||||
pages: PagePool,
|
pages: PagePool,
|
||||||
|
pins: PinPool,
|
||||||
|
|
||||||
pub const ResetMode = std.heap.ArenaAllocator.ResetMode;
|
pub const ResetMode = std.heap.ArenaAllocator.ResetMode;
|
||||||
|
|
||||||
@ -60,17 +67,26 @@ pub const MemoryPool = struct {
|
|||||||
errdefer pool.deinit();
|
errdefer pool.deinit();
|
||||||
var page_pool = try PagePool.initPreheated(page_alloc, preheat);
|
var page_pool = try PagePool.initPreheated(page_alloc, preheat);
|
||||||
errdefer page_pool.deinit();
|
errdefer page_pool.deinit();
|
||||||
return .{ .nodes = pool, .pages = page_pool };
|
var pin_pool = try PinPool.initPreheated(gen_alloc, 8);
|
||||||
|
errdefer pin_pool.deinit();
|
||||||
|
return .{
|
||||||
|
.alloc = gen_alloc,
|
||||||
|
.nodes = pool,
|
||||||
|
.pages = page_pool,
|
||||||
|
.pins = pin_pool,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *MemoryPool) void {
|
pub fn deinit(self: *MemoryPool) void {
|
||||||
self.pages.deinit();
|
self.pages.deinit();
|
||||||
self.nodes.deinit();
|
self.nodes.deinit();
|
||||||
|
self.pins.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(self: *MemoryPool, mode: ResetMode) void {
|
pub fn reset(self: *MemoryPool, mode: ResetMode) void {
|
||||||
_ = self.pages.reset(mode);
|
_ = self.pages.reset(mode);
|
||||||
_ = self.nodes.reset(mode);
|
_ = self.nodes.reset(mode);
|
||||||
|
_ = self.pins.reset(mode);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -91,6 +107,9 @@ page_size: usize,
|
|||||||
/// in a page that also includes scrollback, then that page is not included.
|
/// in a page that also includes scrollback, then that page is not included.
|
||||||
max_size: usize,
|
max_size: usize,
|
||||||
|
|
||||||
|
/// The list of tracked pins. These are kept up to date automatically.
|
||||||
|
tracked_pins: PinSet,
|
||||||
|
|
||||||
/// The top-left of certain parts of the screen that are frequently
|
/// The top-left of certain parts of the screen that are frequently
|
||||||
/// accessed so we don't have to traverse the linked list to find them.
|
/// accessed so we don't have to traverse the linked list to find them.
|
||||||
///
|
///
|
||||||
@ -100,6 +119,11 @@ max_size: usize,
|
|||||||
///
|
///
|
||||||
viewport: Viewport,
|
viewport: Viewport,
|
||||||
|
|
||||||
|
/// The pin used for when the viewport scrolls. This is always pre-allocated
|
||||||
|
/// so that scrolling doesn't have a failable memory allocation. This should
|
||||||
|
/// never be access directly; use `viewport`.
|
||||||
|
viewport_pin: *Pin,
|
||||||
|
|
||||||
/// The current desired screen dimensions. I say "desired" because individual
|
/// The current desired screen dimensions. I say "desired" because individual
|
||||||
/// pages may still be a different size and not yet reflowed since we lazily
|
/// pages may still be a different size and not yet reflowed since we lazily
|
||||||
/// reflow text.
|
/// reflow text.
|
||||||
@ -149,6 +173,7 @@ pub fn init(
|
|||||||
// and we'll split it thereafter if it gets too large and add more as
|
// and we'll split it thereafter if it gets too large and add more as
|
||||||
// necessary.
|
// necessary.
|
||||||
var pool = try MemoryPool.init(alloc, std.heap.page_allocator, page_preheat);
|
var pool = try MemoryPool.init(alloc, std.heap.page_allocator, page_preheat);
|
||||||
|
errdefer pool.deinit();
|
||||||
|
|
||||||
var page = try pool.nodes.create();
|
var page = try pool.nodes.create();
|
||||||
const page_buf = try pool.pages.create();
|
const page_buf = try pool.pages.create();
|
||||||
@ -191,13 +216,18 @@ pub fn init(
|
|||||||
.pages = page_list,
|
.pages = page_list,
|
||||||
.page_size = page_size,
|
.page_size = page_size,
|
||||||
.max_size = max_size_actual,
|
.max_size = max_size_actual,
|
||||||
|
.tracked_pins = .{},
|
||||||
.viewport = .{ .active = {} },
|
.viewport = .{ .active = {} },
|
||||||
|
.viewport_pin = try pool.pins.create(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deinit the pagelist. If you own the memory pool (used clonePool) then
|
/// Deinit the pagelist. If you own the memory pool (used clonePool) then
|
||||||
/// this will reset the pool and retain capacity.
|
/// this will reset the pool and retain capacity.
|
||||||
pub fn deinit(self: *PageList) void {
|
pub fn deinit(self: *PageList) void {
|
||||||
|
// Always deallocate our hashmap.
|
||||||
|
self.tracked_pins.deinit(self.pool.alloc);
|
||||||
|
|
||||||
// Deallocate all the pages. We don't need to deallocate the list or
|
// Deallocate all the pages. We don't need to deallocate the list or
|
||||||
// nodes because they all reside in the pool.
|
// nodes because they all reside in the pool.
|
||||||
if (self.pool_owned) {
|
if (self.pool_owned) {
|
||||||
@ -290,6 +320,11 @@ pub fn clonePool(
|
|||||||
total_rows += len;
|
total_rows += len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Our viewport pin is always undefined since our viewport in a clones
|
||||||
|
// goes back to the top
|
||||||
|
const viewport_pin = try pool.pins.create();
|
||||||
|
errdefer pool.pins.destroy(viewport_pin);
|
||||||
|
|
||||||
var result: PageList = .{
|
var result: PageList = .{
|
||||||
.pool = pool.*,
|
.pool = pool.*,
|
||||||
.pool_owned = false,
|
.pool_owned = false,
|
||||||
@ -298,7 +333,9 @@ pub fn clonePool(
|
|||||||
.max_size = self.max_size,
|
.max_size = self.max_size,
|
||||||
.cols = self.cols,
|
.cols = self.cols,
|
||||||
.rows = self.rows,
|
.rows = self.rows,
|
||||||
|
.tracked_pins = .{}, // TODO
|
||||||
.viewport = .{ .top = {} },
|
.viewport = .{ .top = {} },
|
||||||
|
.viewport_pin = viewport_pin,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We always need to have enough rows for our viewport because this is
|
// We always need to have enough rows for our viewport because this is
|
||||||
@ -1387,6 +1424,20 @@ pub fn eraseRows(
|
|||||||
// be written to again (its in the past) or it will grow and the
|
// be written to again (its in the past) or it will grow and the
|
||||||
// terminal erase will automatically erase the data.
|
// terminal erase will automatically erase the data.
|
||||||
|
|
||||||
|
// Update any tracked pins to shift their y. If it was in the erased
|
||||||
|
// row then we move it to the top of this page.
|
||||||
|
var pin_it = self.tracked_pins.keyIterator();
|
||||||
|
while (pin_it.next()) |p_ptr| {
|
||||||
|
const p = p_ptr.*;
|
||||||
|
if (p.page != chunk.page) continue;
|
||||||
|
if (p.y >= chunk.end) {
|
||||||
|
p.y -= chunk.end;
|
||||||
|
} else {
|
||||||
|
p.y = 0;
|
||||||
|
p.x = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If our viewport is on this page and the offset is beyond
|
// If our viewport is on this page and the offset is beyond
|
||||||
// our new end, shift it.
|
// our new end, shift it.
|
||||||
switch (self.viewport) {
|
switch (self.viewport) {
|
||||||
@ -1434,8 +1485,21 @@ pub fn eraseRows(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Erase a single page, freeing all its resources. The page can be
|
/// Erase a single page, freeing all its resources. The page can be
|
||||||
/// anywhere in the linked list.
|
/// anywhere in the linked list but must NOT be the final page in the
|
||||||
|
/// entire list (i.e. must not make the list empty).
|
||||||
fn erasePage(self: *PageList, page: *List.Node) void {
|
fn erasePage(self: *PageList, page: *List.Node) void {
|
||||||
|
assert(page.next != null or page.prev != null);
|
||||||
|
|
||||||
|
// Update any tracked pins to move to the next page.
|
||||||
|
var it = self.tracked_pins.keyIterator();
|
||||||
|
while (it.next()) |p_ptr| {
|
||||||
|
const p = p_ptr.*;
|
||||||
|
if (p.page != page) continue;
|
||||||
|
p.page = page.next orelse page.prev orelse unreachable;
|
||||||
|
p.y = 0;
|
||||||
|
p.x = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// If our viewport is pinned to this page, then we need to update it.
|
// If our viewport is pinned to this page, then we need to update it.
|
||||||
switch (self.viewport) {
|
switch (self.viewport) {
|
||||||
.top, .active => {},
|
.top, .active => {},
|
||||||
@ -1455,10 +1519,38 @@ fn erasePage(self: *PageList, page: *List.Node) void {
|
|||||||
self.destroyPage(page);
|
self.destroyPage(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the top-left of the screen for the given tag.
|
/// Returns the pin for the given point. The pin is NOT tracked so it
|
||||||
pub fn rowOffset(self: *const PageList, pt: point.Point) RowOffset {
|
/// is only valid as long as the pagelist isn't modified.
|
||||||
// TODO: assert the point is valid
|
pub fn pin(self: *const PageList, pt: point.Point) ?Pin {
|
||||||
return self.getTopLeft(pt).forward(pt.coord().y).?;
|
var p = self.getTopLeft2(pt).down(pt.coord().y) orelse return null;
|
||||||
|
p.x = pt.coord().x;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the given pin to a tracked pin. A tracked pin will always be
|
||||||
|
/// automatically updated as the pagelist is modified. If the point the
|
||||||
|
/// pin points to is removed completely, the tracked pin will be updated
|
||||||
|
/// to the top-left of the screen.
|
||||||
|
pub fn trackPin(self: *PageList, p: Pin) !*Pin {
|
||||||
|
// TODO: assert pin is valid
|
||||||
|
|
||||||
|
// Create our tracked pin
|
||||||
|
const tracked = try self.pool.pins.create();
|
||||||
|
errdefer self.pool.pins.destroy(tracked);
|
||||||
|
tracked.* = p;
|
||||||
|
|
||||||
|
// Add it to the tracked list
|
||||||
|
try self.tracked_pins.putNoClobber(self.pool.alloc, tracked, {});
|
||||||
|
errdefer _ = self.tracked_pins.remove(tracked);
|
||||||
|
|
||||||
|
return tracked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Untrack a previously tracked pin. This will deallocate the pin.
|
||||||
|
pub fn untrackPin(self: *PageList, p: *Pin) void {
|
||||||
|
if (self.tracked_pins.remove(p)) {
|
||||||
|
self.pool.pins.destroy(p);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the cell at the given point, or null if the cell does not
|
/// Get the cell at the given point, or null if the cell does not
|
||||||
@ -1466,14 +1558,14 @@ pub fn rowOffset(self: *const PageList, pt: point.Point) RowOffset {
|
|||||||
///
|
///
|
||||||
/// Warning: this is slow and should not be used in performance critical paths
|
/// Warning: this is slow and should not be used in performance critical paths
|
||||||
pub fn getCell(self: *const PageList, pt: point.Point) ?Cell {
|
pub fn getCell(self: *const PageList, pt: point.Point) ?Cell {
|
||||||
const row = self.getTopLeft(pt).forward(pt.coord().y) orelse return null;
|
const pt_pin = self.pin(pt) orelse return null;
|
||||||
const rac = row.page.data.getRowAndCell(pt.coord().x, row.row_offset);
|
const rac = pt_pin.page.data.getRowAndCell(pt_pin.x, pt_pin.y);
|
||||||
return .{
|
return .{
|
||||||
.page = row.page,
|
.page = pt_pin.page,
|
||||||
.row = rac.row,
|
.row = rac.row,
|
||||||
.cell = rac.cell,
|
.cell = rac.cell,
|
||||||
.row_idx = row.row_offset,
|
.row_idx = pt_pin.y,
|
||||||
.col_idx = pt.coord().x,
|
.col_idx = pt_pin.x,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1686,6 +1778,38 @@ fn getTopLeft(self: *const PageList, tag: point.Tag) RowOffset {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the top-left of the screen for the given tag.
|
||||||
|
fn getTopLeft2(self: *const PageList, tag: point.Tag) Pin {
|
||||||
|
return switch (tag) {
|
||||||
|
// The full screen or history is always just the first page.
|
||||||
|
.screen, .history => .{ .page = self.pages.first.? },
|
||||||
|
|
||||||
|
.viewport => switch (self.viewport) {
|
||||||
|
.active => self.getTopLeft2(.active),
|
||||||
|
.top => self.getTopLeft2(.screen),
|
||||||
|
.exact => |v| .{ .page = v.page, .y = v.row_offset },
|
||||||
|
},
|
||||||
|
|
||||||
|
// The active area is calculated backwards from the last page.
|
||||||
|
// This makes getting the active top left slower but makes scrolling
|
||||||
|
// much faster because we don't need to update the top left. Under
|
||||||
|
// heavy load this makes a measurable difference.
|
||||||
|
.active => active: {
|
||||||
|
var page = self.pages.last.?;
|
||||||
|
var rem = self.rows;
|
||||||
|
while (rem > page.data.size.rows) {
|
||||||
|
rem -= page.data.size.rows;
|
||||||
|
page = page.prev.?; // assertion: we always have enough rows for active
|
||||||
|
}
|
||||||
|
|
||||||
|
break :active .{
|
||||||
|
.page = page,
|
||||||
|
.y = page.data.size.rows - rem,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// The total rows in the screen. This is the actual row count currently
|
/// The total rows in the screen. This is the actual row count currently
|
||||||
/// and not a capacity or maximum.
|
/// and not a capacity or maximum.
|
||||||
///
|
///
|
||||||
@ -1722,6 +1846,108 @@ fn growRows(self: *PageList, n: usize) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents an exact x/y coordinate within the screen. This is called
|
||||||
|
/// a "pin" because it is a fixed point within the pagelist direct to
|
||||||
|
/// a specific page pointer and memory offset. The benefit is that this
|
||||||
|
/// point remains valid even through scrolling without any additional work.
|
||||||
|
///
|
||||||
|
/// A downside is that the pin is only valid until the pagelist is modified
|
||||||
|
/// in a way that may invalid page pointers or shuffle rows, such as resizing,
|
||||||
|
/// erasing rows, etc.
|
||||||
|
///
|
||||||
|
/// A pin can also be "tracked" which means that it will be updated as the
|
||||||
|
/// PageList is modified.
|
||||||
|
///
|
||||||
|
/// The PageList maintains a list of active pin references and keeps them
|
||||||
|
/// all up to date as the pagelist is modified. This isn't cheap so callers
|
||||||
|
/// should limit the number of active pins as much as possible.
|
||||||
|
pub const Pin = struct {
|
||||||
|
page: *List.Node,
|
||||||
|
y: usize = 0,
|
||||||
|
x: usize = 0,
|
||||||
|
|
||||||
|
/// Move the pin down a certain number of rows, or return null if
|
||||||
|
/// the pin goes beyond the end of the screen.
|
||||||
|
pub fn down(self: Pin, n: usize) ?Pin {
|
||||||
|
return switch (self.downOverflow(n)) {
|
||||||
|
.offset => |v| v,
|
||||||
|
.overflow => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the pin up a certain number of rows, or return null if
|
||||||
|
/// the pin goes beyond the start of the screen.
|
||||||
|
pub fn up(self: Pin, n: usize) ?Pin {
|
||||||
|
return switch (self.upOverflow(n)) {
|
||||||
|
.offset => |v| v,
|
||||||
|
.overflow => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the offset down n rows. If the offset goes beyond the
|
||||||
|
/// end of the screen, return the overflow amount.
|
||||||
|
fn downOverflow(self: Pin, n: usize) union(enum) {
|
||||||
|
offset: Pin,
|
||||||
|
overflow: struct {
|
||||||
|
end: Pin,
|
||||||
|
remaining: usize,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
// Index fits within this page
|
||||||
|
const rows = self.page.data.size.rows - (self.y + 1);
|
||||||
|
if (n <= rows) return .{ .offset = .{
|
||||||
|
.page = self.page,
|
||||||
|
.y = n + self.y,
|
||||||
|
} };
|
||||||
|
|
||||||
|
// Need to traverse page links to find the page
|
||||||
|
var page: *List.Node = self.page;
|
||||||
|
var n_left: usize = n - rows;
|
||||||
|
while (true) {
|
||||||
|
page = page.next orelse return .{ .overflow = .{
|
||||||
|
.end = .{ .page = page, .y = page.data.size.rows - 1 },
|
||||||
|
.remaining = n_left,
|
||||||
|
} };
|
||||||
|
if (n_left <= page.data.size.rows) return .{ .offset = .{
|
||||||
|
.page = page,
|
||||||
|
.y = n_left - 1,
|
||||||
|
} };
|
||||||
|
n_left -= page.data.size.rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the offset up n rows. If the offset goes beyond the
|
||||||
|
/// start of the screen, return the overflow amount.
|
||||||
|
fn upOverflow(self: Pin, n: usize) union(enum) {
|
||||||
|
offset: Pin,
|
||||||
|
overflow: struct {
|
||||||
|
end: Pin,
|
||||||
|
remaining: usize,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
// Index fits within this page
|
||||||
|
if (n <= self.y) return .{ .offset = .{
|
||||||
|
.page = self.page,
|
||||||
|
.y = self.y - n,
|
||||||
|
} };
|
||||||
|
|
||||||
|
// Need to traverse page links to find the page
|
||||||
|
var page: *List.Node = self.page;
|
||||||
|
var n_left: usize = n - self.y;
|
||||||
|
while (true) {
|
||||||
|
page = page.prev orelse return .{ .overflow = .{
|
||||||
|
.end = .{ .page = page, .y = 0 },
|
||||||
|
.remaining = n_left,
|
||||||
|
} };
|
||||||
|
if (n_left <= page.data.size.rows) return .{ .offset = .{
|
||||||
|
.page = page,
|
||||||
|
.y = page.data.size.rows - n_left,
|
||||||
|
} };
|
||||||
|
n_left -= page.data.size.rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// Represents some y coordinate within the screen. Since pages can
|
/// Represents some y coordinate within the screen. Since pages can
|
||||||
/// be split at any row boundary, getting some Y-coordinate within
|
/// be split at any row boundary, getting some Y-coordinate within
|
||||||
/// any part of the screen may map to a different page and row offset
|
/// any part of the screen may map to a different page and row offset
|
||||||
@ -1876,10 +2102,11 @@ test "PageList" {
|
|||||||
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
|
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
|
||||||
|
|
||||||
// Active area should be the top
|
// Active area should be the top
|
||||||
try testing.expectEqual(RowOffset{
|
try testing.expectEqual(Pin{
|
||||||
.page = s.pages.first.?,
|
.page = s.pages.first.?,
|
||||||
.row_offset = 0,
|
.y = 0,
|
||||||
}, s.getTopLeft(.active));
|
.x = 0,
|
||||||
|
}, s.getTopLeft2(.active));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "PageList active after grow" {
|
test "PageList active after grow" {
|
||||||
@ -2333,6 +2560,78 @@ test "PageList erase" {
|
|||||||
try testing.expectEqual(s.rows, s.totalRows());
|
try testing.expectEqual(s.rows, s.totalRows());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "PageList erase row with tracked pin resets to top-left" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 80, 24, null);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
// Grow so we take up at least 5 pages.
|
||||||
|
const page = &s.pages.last.?.data;
|
||||||
|
for (0..page.capacity.rows * 5) |_| {
|
||||||
|
_ = try s.grow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our total rows should be large
|
||||||
|
try testing.expect(s.totalRows() > s.rows);
|
||||||
|
|
||||||
|
// Put a tracked pin in the history
|
||||||
|
const p = try s.trackPin(s.pin(.{ .history = .{} }).?);
|
||||||
|
defer s.untrackPin(p);
|
||||||
|
|
||||||
|
// Erase the entire history, we should be back to just our active set.
|
||||||
|
s.eraseRows(.{ .history = .{} }, null);
|
||||||
|
try testing.expectEqual(s.rows, s.totalRows());
|
||||||
|
|
||||||
|
// Our pin should move to the first page
|
||||||
|
try testing.expectEqual(s.pages.first.?, p.page);
|
||||||
|
try testing.expectEqual(@as(usize, 0), p.y);
|
||||||
|
try testing.expectEqual(@as(usize, 0), p.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "PageList erase row with tracked pin shifts" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 80, 24, null);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
// Put a tracked pin in the history
|
||||||
|
const p = try s.trackPin(s.pin(.{ .active = .{ .y = 4, .x = 2 } }).?);
|
||||||
|
defer s.untrackPin(p);
|
||||||
|
|
||||||
|
// Erase only a few rows in our active
|
||||||
|
s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 3 } });
|
||||||
|
try testing.expectEqual(s.rows, s.totalRows());
|
||||||
|
|
||||||
|
// Our pin should move to the first page
|
||||||
|
try testing.expectEqual(s.pages.first.?, p.page);
|
||||||
|
try testing.expectEqual(@as(usize, 0), p.y);
|
||||||
|
try testing.expectEqual(@as(usize, 2), p.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "PageList erase row with tracked pin is erased" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 80, 24, null);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
// Put a tracked pin in the history
|
||||||
|
const p = try s.trackPin(s.pin(.{ .active = .{ .y = 2, .x = 2 } }).?);
|
||||||
|
defer s.untrackPin(p);
|
||||||
|
|
||||||
|
// Erase the entire history, we should be back to just our active set.
|
||||||
|
s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 3 } });
|
||||||
|
try testing.expectEqual(s.rows, s.totalRows());
|
||||||
|
|
||||||
|
// Our pin should move to the first page
|
||||||
|
try testing.expectEqual(s.pages.first.?, p.page);
|
||||||
|
try testing.expectEqual(@as(usize, 0), p.y);
|
||||||
|
try testing.expectEqual(@as(usize, 0), p.x);
|
||||||
|
}
|
||||||
|
|
||||||
test "PageList erase resets viewport to active if moves within active" {
|
test "PageList erase resets viewport to active if moves within active" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
Reference in New Issue
Block a user