terminal/new: PageList.erase

This commit is contained in:
Mitchell Hashimoto
2024-02-27 19:55:34 -08:00
parent 1d30577506
commit 6b5682021e
2 changed files with 105 additions and 2 deletions

View File

@ -359,6 +359,63 @@ fn createPage(self: *PageList) !*List.Node {
return page; return page;
} }
/// Erase the rows from the given top to bottom (inclusive). Erasing
/// the rows doesn't clear them but actually physically REMOVES the rows.
/// If the top or bottom point is in the middle of a page, the other
/// contents in the page will be preserved but the page itself will be
/// underutilized (size < capacity).
pub fn erase(
self: *PageList,
tl_pt: point.Point,
bl_pt: ?point.Point,
) void {
// A rowChunkIterator iterates one page at a time from the back forward.
// "back" here is in terms of scrollback, but actually the front of the
// linked list.
var it = self.rowChunkIterator(tl_pt, bl_pt);
while (it.next()) |chunk| {
// If the chunk is a full page, deinit thit page and remove it from
// the linked list.
if (chunk.fullPage()) {
self.erasePage(chunk.page);
continue;
}
// The chunk is not a full page so we need to move the rows.
// This is a cheap operation because we're just moving cell offsets,
// not the actual cell contents.
assert(chunk.start == 0);
const rows = chunk.page.data.rows.ptr(chunk.page.data.memory);
const scroll_amount = chunk.page.data.size.rows - chunk.end;
for (0..scroll_amount) |i| {
const src: *Row = &rows[i + scroll_amount];
const dst: *Row = &rows[i];
const old_dst = dst.*;
dst.* = src.*;
src.* = old_dst;
}
// We don't even bother deleting the data in the swapped rows
// because erasing in this way yields a page that likely will never
// be written to again (its in the past) or it will grow and the
// terminal erase will automatically erase the data.
chunk.page.data.size.rows = @intCast(scroll_amount);
}
}
/// Erase a single page, freeing all its resources. The page can be
/// anywhere in the linked list.
fn erasePage(self: *PageList, page: *List.Node) void {
// Remove the page from the linked list
self.pages.remove(page);
// Reset the page memory and return it back to the pool.
@memset(page.data.memory, 0);
self.page_pool.destroy(@ptrCast(page.data.memory.ptr));
self.pool.destroy(page);
}
/// Get the top-left of the screen for the given tag. /// Get the top-left of the screen for the given tag.
pub fn rowOffset(self: *const PageList, pt: point.Point) RowOffset { pub fn rowOffset(self: *const PageList, pt: point.Point) RowOffset {
// TODO: assert the point is valid // TODO: assert the point is valid
@ -508,6 +565,11 @@ pub const RowChunkIterator = struct {
const rows_ptr = self.page.data.rows.ptr(self.page.data.memory); const rows_ptr = self.page.data.rows.ptr(self.page.data.memory);
return rows_ptr[self.start..self.end]; return rows_ptr[self.start..self.end];
} }
/// Returns true if this chunk represents every row in the page.
pub fn fullPage(self: Chunk) bool {
return self.start == 0 and self.end == self.page.data.size.rows;
}
}; };
}; };
@ -546,7 +608,12 @@ pub fn rowChunkIterator(
// History goes to the top of the active area. This is more expensive // History goes to the top of the active area. This is more expensive
// to calculate but also more rare of a thing to iterate over. // to calculate but also more rare of a thing to iterate over.
.history => .{ .row = self.getTopLeft(.active) }, .history => history: {
const active_tl = self.getTopLeft(.active);
const history_bot = active_tl.backward(1) orelse
return .{ .row = null };
break :history .{ .row = history_bot };
},
}; };
}; };
@ -1187,7 +1254,28 @@ test "PageList rowChunkIterator history two pages" {
try testing.expect(chunk.page == s.pages.first.?); try testing.expect(chunk.page == s.pages.first.?);
const start: usize = 0; const start: usize = 0;
try testing.expectEqual(start, chunk.start); try testing.expectEqual(start, chunk.start);
try testing.expectEqual(active_tl.row_offset + 1, chunk.end); try testing.expectEqual(active_tl.row_offset, chunk.end);
} }
try testing.expect(it.next() == null); try testing.expect(it.next() == null);
} }
test "PageList erase" {
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);
// Erase the entire history, we should be back to just our active set.
s.erase(.{ .history = .{} }, null);
try testing.expectEqual(s.rows, s.totalRows());
}

View File

@ -4,6 +4,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const assert = std.debug.assert; const assert = std.debug.assert;
const ansi = @import("../ansi.zig"); const ansi = @import("../ansi.zig");
const kitty = @import("../kitty.zig");
const sgr = @import("../sgr.zig"); const sgr = @import("../sgr.zig");
const unicode = @import("../../unicode/main.zig"); const unicode = @import("../../unicode/main.zig");
const PageList = @import("PageList.zig"); const PageList = @import("PageList.zig");
@ -35,6 +36,9 @@ saved_cursor: ?SavedCursor = null,
/// protection mode since some sequences such as ECH depend on this. /// protection mode since some sequences such as ECH depend on this.
protected_mode: ansi.ProtectedMode = .off, protected_mode: ansi.ProtectedMode = .off,
/// Kitty graphics protocol state.
kitty_images: kitty.graphics.ImageStorage = .{},
/// The cursor position. /// The cursor position.
pub const Cursor = struct { pub const Cursor = struct {
// The x/y position within the viewport. // The x/y position within the viewport.
@ -113,6 +117,7 @@ pub fn init(
} }
pub fn deinit(self: *Screen) void { pub fn deinit(self: *Screen) void {
self.kitty_images.deinit(self.alloc);
self.pages.deinit(); self.pages.deinit();
} }
@ -270,6 +275,11 @@ pub const Scroll = union(enum) {
/// Scroll the viewport of the terminal grid. /// Scroll the viewport of the terminal grid.
pub fn scroll(self: *Screen, behavior: Scroll) void { pub fn scroll(self: *Screen, behavior: Scroll) void {
// No matter what, scrolling marks our image state as dirty since
// it could move placements. If there are no placements or no images
// this is still a very cheap operation.
self.kitty_images.dirty = true;
switch (behavior) { switch (behavior) {
.active => self.pages.scroll(.{ .active = {} }), .active => self.pages.scroll(.{ .active = {} }),
.top => self.pages.scroll(.{ .top = {} }), .top => self.pages.scroll(.{ .top = {} }),
@ -282,6 +292,11 @@ pub fn scroll(self: *Screen, behavior: Scroll) void {
pub fn scrollClear(self: *Screen) !void { pub fn scrollClear(self: *Screen) !void {
try self.pages.scrollClear(); try self.pages.scrollClear();
self.cursorAbsolute(0, 0); self.cursorAbsolute(0, 0);
// No matter what, scrolling marks our image state as dirty since
// it could move placements. If there are no placements or no images
// this is still a very cheap operation.
self.kitty_images.dirty = true;
} }
// Erase the region specified by tl and bl, inclusive. Erased cells are // Erase the region specified by tl and bl, inclusive. Erased cells are