terminal/new: Screen.clone

This commit is contained in:
Mitchell Hashimoto
2024-02-28 21:35:01 -08:00
parent daf113b147
commit bda44f9b0c
3 changed files with 270 additions and 10 deletions

View File

@ -180,6 +180,12 @@ pub fn deinit(self: *PageList) void {
}
/// Clone this pagelist from the top to bottom (inclusive).
///
/// The viewport is always moved to the top-left.
///
/// The cloned pagelist must contain at least enough rows for the active
/// area. If the region specified has less rows than the active area then
/// rows will be added to the bottom of the region to make up the difference.
pub fn clone(
self: *const PageList,
alloc: Allocator,
@ -204,20 +210,75 @@ pub fn clone(
errdefer page_pool.deinit();
// Copy our pages
const page_list: List = .{};
var page_list: List = .{};
var total_rows: usize = 0;
while (it.next()) |chunk| {
_ = chunk;
// Clone the page
const page = try pool.create();
const page_buf = try page_pool.create();
page.* = .{ .data = chunk.page.data.cloneBuf(page_buf) };
page_list.append(page);
// If this is a full page then we're done.
if (chunk.fullPage()) {
total_rows += page.data.size.rows;
continue;
}
// If this is just a shortened chunk off the end we can just
// shorten the size. We don't worry about clearing memory here because
// as the page grows the memory will be reclaimable because the data
// is still valid.
if (chunk.start == 0) {
page.data.size.rows = @intCast(chunk.end);
total_rows += chunk.end;
continue;
}
// Kind of slow, we want to shift the rows up in the page up to
// end and then resize down.
const rows = page.data.rows.ptr(page.data.memory);
const len = chunk.end - chunk.start;
for (0..len) |i| {
const src: *Row = &rows[i + chunk.start];
const dst: *Row = &rows[i];
const old_dst = dst.*;
dst.* = src.*;
src.* = old_dst;
}
page.data.size.rows = @intCast(len);
total_rows += len;
}
return .{
var result: PageList = .{
.alloc = alloc,
.pool = pool,
.page_pool = page_pool,
.pages = page_list,
.page_size = PagePool.item_size * page_count,
.max_size = self.max_size,
.cols = self.cols,
.rows = self.rows,
.viewport = .{ .top = {} },
};
// We always need to have enough rows for our viewport because this is
// a pagelist invariant that other code relies on.
if (total_rows < self.rows) {
const len = self.rows - total_rows;
for (0..len) |_| {
_ = try result.grow();
// Clear the row. This is not very fast but in reality right
// now we rarely clone less than the active area and if we do
// the area is by definition very small.
const last = result.pages.last.?;
const row = &last.data.rows.ptr(last.data.memory)[last.data.size.rows - 1];
last.data.clearCells(row, 0, result.cols);
}
}
return result;
}
/// Scroll options.
@ -1412,3 +1473,87 @@ test "PageList erase active regrows automatically" {
s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 10 } });
try testing.expect(s.totalRows() == s.rows);
}
test "PageList clone" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 80, 24, null);
defer s.deinit();
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
var s2 = try s.clone(alloc, .{ .screen = .{} }, null);
defer s2.deinit();
try testing.expectEqual(@as(usize, s.rows), s2.totalRows());
}
test "PageList clone partial trimmed right" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 80, 20, null);
defer s.deinit();
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
try s.growRows(30);
var s2 = try s.clone(
alloc,
.{ .screen = .{} },
.{ .screen = .{ .y = 39 } },
);
defer s2.deinit();
try testing.expectEqual(@as(usize, 40), s2.totalRows());
}
test "PageList clone partial trimmed left" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 80, 20, null);
defer s.deinit();
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
try s.growRows(30);
var s2 = try s.clone(
alloc,
.{ .screen = .{ .y = 10 } },
null,
);
defer s2.deinit();
try testing.expectEqual(@as(usize, 40), s2.totalRows());
}
test "PageList clone partial trimmed both" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 80, 20, null);
defer s.deinit();
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
try s.growRows(30);
var s2 = try s.clone(
alloc,
.{ .screen = .{ .y = 10 } },
.{ .screen = .{ .y = 35 } },
);
defer s2.deinit();
try testing.expectEqual(@as(usize, 26), s2.totalRows());
}
test "PageList clone less than active" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 80, 24, null);
defer s.deinit();
try testing.expectEqual(@as(usize, s.rows), s.totalRows());
var s2 = try s.clone(
alloc,
.{ .active = .{ .y = 5 } },
null,
);
defer s2.deinit();
try testing.expectEqual(@as(usize, s.rows), s2.totalRows());
}

View File

@ -166,8 +166,20 @@ pub fn deinit(self: *Screen) void {
///
/// - Screen dimensions
/// - Screen data (cell state, etc.) for the region
/// - Cursor if its in the region. If the cursor is not in the region
/// then it will be placed at the top-left of the new screen.
///
/// Anything not mentioned above is NOT copied. Some of this is for
/// very good reason:
///
/// - Kitty images have a LOT of data. This is not efficient to copy.
/// Use a lock and access the image data. The dirty bit is there for
/// a reason.
/// - Cursor location can be expensive to calculate with respect to the
/// specified region. It is faster to grab the cursor from the old
/// screen and then move it to the new screen.
///
/// If not mentioned above, then there isn't a specific reason right now
/// to not copy some data other than we probably didn't need it and it
/// isn't necessary for screen coherency.
///
/// Other notes:
///
@ -180,12 +192,19 @@ pub fn clone(
self: *const Screen,
alloc: Allocator,
top: point.Point,
bottom: ?point.Point,
bot: ?point.Point,
) !Screen {
_ = self;
_ = alloc;
_ = top;
_ = bottom;
var pages = try self.pages.clone(alloc, top, bot);
errdefer pages.deinit();
return .{
.alloc = alloc,
.pages = pages,
.no_scrollback = self.no_scrollback,
// TODO: let's make this reasonble
.cursor = undefined,
};
}
pub fn cursorCellRight(self: *Screen, n: size.CellCountInt) *pagepkg.Cell {
@ -1431,3 +1450,62 @@ test "Screen: scroll and clear ignore blank lines" {
try testing.expectEqualStrings("1ABCD\n2EFGH\n3ABCD\nX", contents);
}
}
test "Screen: clone" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 10);
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH");
{
const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings("1ABCD\n2EFGH", contents);
}
// Clone
var s2 = try s.clone(alloc, .{ .active = .{} }, null);
defer s2.deinit();
{
const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings("1ABCD\n2EFGH", contents);
}
// Write to s1, should not be in s2
try s.testWriteString("\n34567");
{
const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings("1ABCD\n2EFGH\n34567", contents);
}
{
const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings("1ABCD\n2EFGH", contents);
}
}
test "Screen: clone partial" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 10);
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH");
{
const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings("1ABCD\n2EFGH", contents);
}
// Clone
var s2 = try s.clone(alloc, .{ .active = .{ .y = 1 } }, null);
defer s2.deinit();
{
const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings("2EFGH", contents);
}
}

View File

@ -266,6 +266,43 @@ pub const Page = struct {
@panic("TODO: grapheme move");
}
/// Clear the cells in the given row. This will reclaim memory used
/// by graphemes and styles. Note that if the style cleared is still
/// active, Page cannot know this and it will still be ref counted down.
/// The best solution for this is to artificially increment the ref count
/// prior to calling this function.
pub fn clearCells(
self: *Page,
row: *Row,
left: usize,
end: usize,
) void {
const cells = row.cells.ptr(self.memory)[left..end];
if (row.grapheme) {
for (cells) |*cell| {
if (cell.hasGrapheme()) self.clearGrapheme(row, cell);
}
}
if (row.styled) {
for (cells) |*cell| {
if (cell.style_id == style.default_id) continue;
if (self.styles.lookupId(self.memory, cell.style_id)) |prev_style| {
// Below upsert can't fail because it should already be present
const md = self.styles.upsert(self.memory, prev_style.*) catch unreachable;
assert(md.ref > 0);
md.ref -= 1;
if (md.ref == 0) self.styles.remove(self.memory, cell.style_id);
}
}
if (cells.len == self.size.cols) row.styled = false;
}
@memset(cells, .{});
}
/// Append a codepoint to the given cell as a grapheme.
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) !void {
if (comptime std.debug.runtime_safety) assert(cell.hasText());