diff --git a/src/terminal/new/PageList.zig b/src/terminal/new/PageList.zig index 1e8004b51..9de7fa889 100644 --- a/src/terminal/new/PageList.zig +++ b/src/terminal/new/PageList.zig @@ -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()); +} diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index 0430c1c00..30021baf8 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -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); + } +} diff --git a/src/terminal/new/page.zig b/src/terminal/new/page.zig index 60346c1af..5673e2c90 100644 --- a/src/terminal/new/page.zig +++ b/src/terminal/new/page.zig @@ -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());