//! Maintains a linked list of pages to make up a terminal screen //! and provides higher level operations on top of those pages to //! make it slightly easier to work with. const PageList = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const point = @import("point.zig"); const pagepkg = @import("page.zig"); const stylepkg = @import("style.zig"); const size = @import("size.zig"); const OffsetBuf = size.OffsetBuf; const Capacity = pagepkg.Capacity; const Page = pagepkg.Page; const Row = pagepkg.Row; const log = std.log.scoped(.page_list); /// The number of PageList.Nodes we preheat the pool with. A node is /// a very small struct so we can afford to preheat many, but the exact /// number is uncertain. Any number too large is wasting memory, any number /// too small will cause the pool to have to allocate more memory later. /// This should be set to some reasonable minimum that we expect a terminal /// window to scroll into quickly. const page_preheat = 4; /// The list of pages in the screen. These are expected to be in order /// where the first page is the topmost page (scrollback) and the last is /// the bottommost page (the current active page). const List = std.DoublyLinkedList(Page); /// The memory pool we get page nodes from. const NodePool = std.heap.MemoryPool(List.Node); const std_capacity = pagepkg.std_capacity; /// The memory pool we use for page memory buffers. We use a separate pool /// so we can allocate these with a page allocator. We have to use a page /// allocator because we need memory that is zero-initialized and page-aligned. const PagePool = std.heap.MemoryPoolAligned( [Page.layout(std_capacity).total_size]u8, 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 /// multiple pagelists but it is not threadsafe. pub const MemoryPool = struct { alloc: Allocator, nodes: NodePool, pages: PagePool, pins: PinPool, pub const ResetMode = std.heap.ArenaAllocator.ResetMode; pub fn init( gen_alloc: Allocator, page_alloc: Allocator, preheat: usize, ) !MemoryPool { var pool = try NodePool.initPreheated(gen_alloc, preheat); errdefer pool.deinit(); var page_pool = try PagePool.initPreheated(page_alloc, preheat); errdefer page_pool.deinit(); 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 { self.pages.deinit(); self.nodes.deinit(); self.pins.deinit(); } pub fn reset(self: *MemoryPool, mode: ResetMode) void { _ = self.pages.reset(mode); _ = self.nodes.reset(mode); _ = self.pins.reset(mode); } }; /// The memory pool we get page nodes, pages from. pool: MemoryPool, pool_owned: bool, /// The list of pages in the screen. pages: List, /// Byte size of the total amount of allocated pages. Note this does /// not include the total allocated amount in the pool which may be more /// than this due to preheating. page_size: usize, /// Maximum size of the page allocation in bytes. This only includes pages /// that are used ONLY for scrollback. If the active area is still partially /// in a page that also includes scrollback, then that page is not included. 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 /// accessed so we don't have to traverse the linked list to find them. /// /// For other tags, don't need this: /// - screen: pages.first /// - history: active row minus one /// 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 /// pages may still be a different size and not yet reflowed since we lazily /// reflow text. cols: size.CellCountInt, rows: size.CellCountInt, /// The viewport location. pub const Viewport = union(enum) { /// The viewport is pinned to the active area. By using a specific marker /// for this instead of tracking the row offset, we eliminate a number of /// memory writes making scrolling faster. active, /// The viewport is pinned to the top of the screen, or the farthest /// back in the scrollback history. top, /// The viewport is pinned to a tracked pin. The tracked pin is ALWAYS /// s.viewport_pin hence this has no value. We force that value to prevent /// allocations. pin, }; /// Initialize the page. The top of the first page in the list is always the /// top of the active area of the screen (important knowledge for quickly /// setting up cursors in Screen). /// /// max_size is the maximum number of bytes that will be allocated for /// pages. If this is smaller than the bytes required to show the viewport /// then max_size will be ignored and the viewport will be shown, but no /// scrollback will be created. max_size is always rounded down to the nearest /// terminal page size (not virtual memory page), otherwise we would always /// slightly exceed max_size in the limits. /// /// If max_size is null then there is no defined limit and the screen will /// grow forever. In reality, the limit is set to the byte limit that your /// computer can address in memory. If you somehow require more than that /// (due to disk paging) then please contribute that yourself and perhaps /// search deep within yourself to find out why you need that. pub fn init( alloc: Allocator, cols: size.CellCountInt, rows: size.CellCountInt, max_size: ?usize, ) !PageList { // The screen starts with a single page that is the entire viewport, // and we'll split it thereafter if it gets too large and add more as // necessary. var pool = try MemoryPool.init(alloc, std.heap.page_allocator, page_preheat); errdefer pool.deinit(); var page = try pool.nodes.create(); const page_buf = try pool.pages.create(); // no errdefer because the pool deinit will clean these up // In runtime safety modes we have to memset because the Zig allocator // interface will always memset to 0xAA for undefined. In non-safe modes // we use a page allocator and the OS guarantees zeroed memory. if (comptime std.debug.runtime_safety) @memset(page_buf, 0); // Initialize the first set of pages to contain our viewport so that // the top of the first page is always the active area. page.* = .{ .data = Page.initBuf( OffsetBuf.init(page_buf), Page.layout(try std_capacity.adjust(.{ .cols = cols })), ), }; assert(page.data.capacity.rows >= rows); // todo: handle this page.data.size.rows = rows; var page_list: List = .{}; page_list.prepend(page); const page_size = page_buf.len; // The max size has to be adjusted to at least fit one viewport. // We use item_size*2 because the active area can always span two // pages as we scroll, otherwise we'd have to constantly copy in the // small limit case. const max_size_actual = @max( max_size orelse std.math.maxInt(usize), PagePool.item_size * 2, ); // We always track our viewport pin to ensure this is never an allocation const viewport_pin = try pool.pins.create(); var tracked_pins: PinSet = .{}; errdefer tracked_pins.deinit(pool.alloc); try tracked_pins.putNoClobber(pool.alloc, viewport_pin, {}); return .{ .cols = cols, .rows = rows, .pool = pool, .pool_owned = true, .pages = page_list, .page_size = page_size, .max_size = max_size_actual, .tracked_pins = tracked_pins, .viewport = .{ .active = {} }, .viewport_pin = viewport_pin, }; } /// Deinit the pagelist. If you own the memory pool (used clonePool) then /// this will reset the pool and retain capacity. 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 // nodes because they all reside in the pool. if (self.pool_owned) { self.pool.deinit(); } else { self.pool.reset(.{ .retain_capacity = {} }); } } /// 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, top: point.Point, bot: ?point.Point, ) !PageList { // First, count our pages so our preheat is exactly what we need. var it = self.pageIterator(top, bot); const page_count: usize = page_count: { var count: usize = 0; while (it.next()) |_| count += 1; break :page_count count; }; // Setup our pools var pool = try MemoryPool.init(alloc, std.heap.page_allocator, page_count); errdefer pool.deinit(); var result = try self.clonePool(&pool, top, bot); result.pool_owned = true; return result; } /// Like clone, but specify your own memory pool. This is advanced but /// lets you avoid expensive syscalls to allocate memory. pub fn clonePool( self: *const PageList, pool: *MemoryPool, top: point.Point, bot: ?point.Point, ) !PageList { var it = self.pageIterator(top, bot); // Copy our pages var page_list: List = .{}; var total_rows: usize = 0; var page_count: usize = 0; while (it.next()) |chunk| { // Clone the page const page = try pool.nodes.create(); const page_buf = try pool.pages.create(); page.* = .{ .data = chunk.page.data.cloneBuf(page_buf) }; page_list.append(page); page_count += 1; // 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; } // 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 = .{ .pool = pool.*, .pool_owned = false, .pages = page_list, .page_size = PagePool.item_size * page_count, .max_size = self.max_size, .cols = self.cols, .rows = self.rows, .tracked_pins = .{}, // TODO .viewport = .{ .top = {} }, .viewport_pin = viewport_pin, }; // 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; } /// Resize options pub const Resize = struct { /// The new cols/cells of the screen. cols: ?size.CellCountInt = null, rows: ?size.CellCountInt = null, /// Whether to reflow the text. If this is false then the text will /// be truncated if the new size is smaller than the old size. reflow: bool = true, /// Set this to a cursor position and the resize will retain the /// cursor position and update this so that the cursor remains over /// the same original cell in the reflowed environment. cursor: ?*Cursor = null, pub const Cursor = struct { x: size.CellCountInt, y: size.CellCountInt, /// The row offset of the cursor. This is assumed to be correct /// if set. If this is not set, then the row offset will be /// calculated from the x/y. Calculating the row offset is expensive /// so if you have it, you should set it. offset: ?RowOffset = null, }; }; /// Resize /// TODO: docs pub fn resize(self: *PageList, opts: Resize) !void { if (!opts.reflow) return try self.resizeWithoutReflow(opts); // On reflow, the main thing that causes reflow is column changes. If // only rows change, reflow is impossible. So we change our behavior based // on the change of columns. const cols = opts.cols orelse self.cols; switch (std.math.order(cols, self.cols)) { .eq => try self.resizeWithoutReflow(opts), .gt => { // We grow rows after cols so that we can do our unwrapping/reflow // before we do a no-reflow grow. try self.resizeCols(cols, opts.cursor); try self.resizeWithoutReflow(opts); }, .lt => { // We first change our row count so that we have the proper amount // we can use when shrinking our cols. try self.resizeWithoutReflow(opts: { var copy = opts; copy.cols = self.cols; break :opts copy; }); try self.resizeCols(cols, opts.cursor); }, } } /// Resize the pagelist with reflow by adding or removing columns. fn resizeCols( self: *PageList, cols: size.CellCountInt, cursor: ?*Resize.Cursor, ) !void { assert(cols != self.cols); // Our new capacity, ensure we can fit the cols const cap = try std_capacity.adjust(.{ .cols = cols }); // If we are given a cursor, we need to calculate the row offset. if (cursor) |c| { if (c.offset == null) { const tl = self.getTopLeft(.active); c.offset = tl.forward(c.y) orelse fail: { // This should never happen, but its not critical enough to // set an assertion and fail the program. The caller should ALWAYS // input a valid x/y.. log.err("cursor offset not found, resize will set wrong cursor", .{}); break :fail null; }; } } // Go page by page and shrink the columns on a per-page basis. var it = self.pageIterator(.{ .screen = .{} }, null); while (it.next()) |chunk| { // Fast-path: none of our rows are wrapped. In this case we can // treat this like a no-reflow resize. This only applies if we // are growing columns. if (cols > self.cols) { const page = &chunk.page.data; const rows = page.rows.ptr(page.memory)[0..page.size.rows]; const wrapped = wrapped: for (rows) |row| { assert(!row.wrap_continuation); // TODO if (row.wrap) break :wrapped true; } else false; if (!wrapped) { try self.resizeWithoutReflowGrowCols(cap, chunk, cursor); continue; } } // Note: we can do a fast-path here if all of our rows in this // page already fit within the new capacity. In that case we can // do a non-reflow resize. try self.reflowPage(cap, chunk.page, cursor); } // If our total rows is less than our active rows, we need to grow. // This can happen if you're growing columns such that enough active // rows unwrap that we no longer have enough. var node_it = self.pages.first; var total: usize = 0; while (node_it) |node| : (node_it = node.next) { total += node.data.size.rows; if (total >= self.rows) break; } else { for (total..self.rows) |_| _ = try self.grow(); } // If we have a cursor, we need to update the correct y value. I'm // not at all happy about this, I wish we could do this in a more // efficient way as we resize the pages. But at the time of typing this // I can't think of a way and I'd rather get things working. Someone please // help! // // The challenge is that as rows are unwrapped, we want to preserve the // cursor. So for examle if you have "A\nB" where AB is soft-wrapped and // the cursor is on 'B' (x=0, y=1) and you grow the columns, we want // the cursor to remain on B (x=1, y=0) as it grows. // // The easy thing to do would be to count how many rows we unwrapped // and then subtract that from the original y. That's how I started. The // challenge is that if we unwrap with scrollback, our scrollback is // "pulled down" so that the original (x=0,y=0) line is now pushed down. // Detecting this while resizing seems non-obvious. This is a tested case // so if you change this logic, you should see failures or passes if it // works. // // The approach I take instead is if we have a cursor offset, I work // backwards to find the offset we marked while reflowing and update // the y from that. This is _not terrible_ because active areas are // generally small and this is a more or less linear search. Its just // kind of clunky. if (cursor) |c| cursor: { const offset = c.offset orelse break :cursor; var active_it = self.rowIterator(.{ .active = .{} }, null); var y: size.CellCountInt = 0; while (active_it.next()) |it_offset| { if (it_offset.page == offset.page and it_offset.row_offset == offset.row_offset) { c.y = y; break :cursor; } y += 1; } else { // Cursor moved off the screen into the scrollback. c.x = 0; c.y = 0; } } // Update our cols self.cols = cols; } // We use a cursor to track where we are in the src/dst. This is very // similar to Screen.Cursor, so see that for docs on individual fields. // We don't use a Screen because we don't need all the same data and we // do our best to optimize having direct access to the page memory. const ReflowCursor = struct { x: size.CellCountInt, y: size.CellCountInt, pending_wrap: bool, page: *pagepkg.Page, page_row: *pagepkg.Row, page_cell: *pagepkg.Cell, fn init(page: *pagepkg.Page) ReflowCursor { const rows = page.rows.ptr(page.memory); return .{ .x = 0, .y = 0, .pending_wrap = false, .page = page, .page_row = &rows[0], .page_cell = &rows[0].cells.ptr(page.memory)[0], }; } fn cursorForward(self: *ReflowCursor) void { if (self.x == self.page.size.cols - 1) { self.pending_wrap = true; } else { const cell: [*]pagepkg.Cell = @ptrCast(self.page_cell); self.page_cell = @ptrCast(cell + 1); self.x += 1; } } fn cursorScroll(self: *ReflowCursor) void { // Scrolling requires that we're on the bottom of our page. // We also assert that we have capacity because reflow always // works within the capacity of the page. assert(self.y == self.page.size.rows - 1); assert(self.page.size.rows < self.page.capacity.rows); // Increase our page size self.page.size.rows += 1; // With the increased page size, safely move down a row. const rows: [*]pagepkg.Row = @ptrCast(self.page_row); const row: *pagepkg.Row = @ptrCast(rows + 1); self.page_row = row; self.page_cell = &row.cells.ptr(self.page.memory)[0]; self.pending_wrap = false; self.x = 0; self.y += 1; } fn cursorAbsolute( self: *ReflowCursor, x: size.CellCountInt, y: size.CellCountInt, ) void { assert(x < self.page.size.cols); assert(y < self.page.size.rows); const rows: [*]pagepkg.Row = @ptrCast(self.page_row); const row: *pagepkg.Row = switch (std.math.order(y, self.y)) { .eq => self.page_row, .lt => @ptrCast(rows - (self.y - y)), .gt => @ptrCast(rows + (y - self.y)), }; self.page_row = row; self.page_cell = &row.cells.ptr(self.page.memory)[x]; self.pending_wrap = false; self.x = x; self.y = y; } fn countTrailingEmptyCells(self: *const ReflowCursor) usize { // If the row is wrapped, all empty cells are meaningful. if (self.page_row.wrap) return 0; const cells: [*]pagepkg.Cell = @ptrCast(self.page_cell); const len: usize = self.page.size.cols - self.x; for (0..len) |i| { const rev_i = len - i - 1; if (!cells[rev_i].isEmpty()) return i; } // If the row has a semantic prompt then the blank row is meaningful // so we always return all but one so that the row is drawn. if (self.page_row.semantic_prompt != .unknown) return len - 1; return len; } fn copyRowMetadata(self: *ReflowCursor, other: *const Row) void { self.page_row.semantic_prompt = other.semantic_prompt; } }; /// Reflow the given page into the new capacity. The new capacity can have /// any number of columns and rows. This will create as many pages as /// necessary to fit the reflowed text and will remove the old page. /// /// Note a couple edge cases: /// /// 1. If the first set of rows of this page are a wrap continuation, then /// we will reflow the continuation rows but will not traverse back to /// find the initial wrap. /// /// 2. If the last row is wrapped then we will traverse forward to reflow /// all the continuation rows. /// /// As a result of the above edge cases, the pagelist may end up removing /// an indefinite number of pages. In the most pathological cases (the screen /// is one giant wrapped line), this can be a very expensive operation. That /// doesn't really happen in typical terminal usage so its not a case we /// optimize for today. Contributions welcome to optimize this. /// /// Conceptually, this is a simple process: we're effectively traversing /// the old page and rewriting into the new page as if it were a text editor. /// But, due to the edge cases, cursor tracking, and attempts at efficiency, /// the code can be convoluted so this is going to be a heavily commented /// function. fn reflowPage( self: *PageList, cap: Capacity, node: *List.Node, cursor: ?*Resize.Cursor, ) !void { // The cursor tracks where we are in the source page. var src_cursor = ReflowCursor.init(&node.data); // This is used to count blank lines so that we don't copy those. var blank_lines: usize = 0; // Our new capacity when growing columns may also shrink rows. So we // need to do a loop in order to potentially make multiple pages. while (true) { // Create our new page and our cursor restarts at 0,0 in the new page. // The new page always starts with a size of 1 because we know we have // at least one row to copy from the src. const dst_node = try self.createPage(cap); dst_node.data.size.rows = 1; var dst_cursor = ReflowCursor.init(&dst_node.data); dst_cursor.copyRowMetadata(src_cursor.page_row); // Copy some initial metadata about the row //dst_cursor.page_row.semantic_prompt = src_cursor.page_row.semantic_prompt; // Our new page goes before our src node. This will append it to any // previous pages we've created. self.pages.insertBefore(node, dst_node); // Continue traversing the source until we're out of space in our // destination or we've copied all our intended rows. for (src_cursor.y..src_cursor.page.size.rows) |src_y| { const prev_wrap = src_cursor.page_row.wrap; src_cursor.cursorAbsolute(0, @intCast(src_y)); // Trim trailing empty cells if the row is not wrapped. If the // row is wrapped then we don't trim trailing empty cells because // the empty cells can be meaningful. const trailing_empty = src_cursor.countTrailingEmptyCells(); const cols_len = src_cursor.page.size.cols - trailing_empty; if (cols_len == 0) { // If the row is empty, we don't copy it. We count it as a // blank line and continue to the next row. blank_lines += 1; continue; } // We have data, if we have blank lines we need to create them first. for (0..blank_lines) |_| { dst_cursor.cursorScroll(); } if (src_y > 0) { // We're done with this row, if this row isn't wrapped, we can // move our destination cursor to the next row. // // The blank_lines == 0 condition is because if we were prefixed // with blank lines, we handled the scroll already above. if (!prev_wrap and blank_lines == 0) { dst_cursor.cursorScroll(); } dst_cursor.copyRowMetadata(src_cursor.page_row); } // Reset our blank line count since handled it all above. blank_lines = 0; for (src_cursor.x..cols_len) |src_x| { assert(src_cursor.x == src_x); // std.log.warn("src_y={} src_x={} dst_y={} dst_x={} cp={u}", .{ // src_cursor.y, // src_cursor.x, // dst_cursor.y, // dst_cursor.x, // src_cursor.page_cell.content.codepoint, // }); // If we have a wide char at the end of our page we need // to insert a spacer head and wrap. if (cap.cols > 1 and src_cursor.page_cell.wide == .wide and dst_cursor.x == cap.cols - 1) { self.reflowUpdateCursor(cursor, &src_cursor, &dst_cursor, dst_node); dst_cursor.page_cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 0 }, .wide = .spacer_head, }; dst_cursor.cursorForward(); } // If we have a spacer head and we're not at the end then // we want to unwrap it and eliminate the head. if (cap.cols > 1 and src_cursor.page_cell.wide == .spacer_head and dst_cursor.x != cap.cols - 1) { self.reflowUpdateCursor(cursor, &src_cursor, &dst_cursor, dst_node); src_cursor.cursorForward(); continue; } if (dst_cursor.pending_wrap) { dst_cursor.page_row.wrap = true; dst_cursor.cursorScroll(); dst_cursor.page_row.wrap_continuation = true; dst_cursor.copyRowMetadata(src_cursor.page_row); } // A rare edge case. If we're resizing down to 1 column // and the source is a non-narrow character, we reset the // cell to a narrow blank and we skip to the next cell. if (cap.cols == 1 and src_cursor.page_cell.wide != .narrow) { switch (src_cursor.page_cell.wide) { .narrow => unreachable, // Wide char, we delete it, reset it to narrow, // and skip forward. .wide => { dst_cursor.page_cell.content.codepoint = 0; dst_cursor.page_cell.wide = .narrow; src_cursor.cursorForward(); continue; }, // Skip spacer tails since we should've already // handled them in the previous cell. .spacer_tail => {}, // TODO: test? .spacer_head => {}, } } else { switch (src_cursor.page_cell.content_tag) { // These are guaranteed to have no styling data and no // graphemes, a fast path. .bg_color_palette, .bg_color_rgb, => { assert(!src_cursor.page_cell.hasStyling()); assert(!src_cursor.page_cell.hasGrapheme()); dst_cursor.page_cell.* = src_cursor.page_cell.*; }, .codepoint => { dst_cursor.page_cell.* = src_cursor.page_cell.*; }, .codepoint_grapheme => { // We copy the cell like normal but we have to reset the // tag because this is used for fast-path detection in // appendGrapheme. dst_cursor.page_cell.* = src_cursor.page_cell.*; dst_cursor.page_cell.content_tag = .codepoint; // Copy the graphemes const src_cps = src_cursor.page.lookupGrapheme(src_cursor.page_cell).?; for (src_cps) |cp| { try dst_cursor.page.appendGrapheme( dst_cursor.page_row, dst_cursor.page_cell, cp, ); } }, } // If the source cell has a style, we need to copy it. if (src_cursor.page_cell.style_id != stylepkg.default_id) { const src_style = src_cursor.page.styles.lookupId( src_cursor.page.memory, src_cursor.page_cell.style_id, ).?.*; const dst_md = try dst_cursor.page.styles.upsert( dst_cursor.page.memory, src_style, ); dst_md.ref += 1; dst_cursor.page_cell.style_id = dst_md.id; } } // If our original cursor was on this page, this x/y then // we need to update to the new location. self.reflowUpdateCursor(cursor, &src_cursor, &dst_cursor, dst_node); // Move both our cursors forward src_cursor.cursorForward(); dst_cursor.cursorForward(); } else cursor: { // We made it through all our source columns. As a final edge // case, if our cursor is in one of the blanks, we update it // to the edge of this page. // If we have no trailing empty cells, it can't be in the blanks. if (trailing_empty == 0) break :cursor; // Update all our tracked pins var it = self.tracked_pins.keyIterator(); while (it.next()) |p_ptr| { const p = p_ptr.*; if (&p.page.data != src_cursor.page or p.y != src_cursor.y or p.x < cols_len) continue; p.page = dst_node; p.y = dst_cursor.y; } // If we have no cursor, nothing to update. const c = cursor orelse break :cursor; const offset = c.offset orelse break :cursor; // If our cursor is on this page, and our x is greater than // our end, we update to the edge. if (&offset.page.data == src_cursor.page and offset.row_offset == src_cursor.y and c.x >= cols_len) { c.offset = .{ .page = dst_node, .row_offset = dst_cursor.y, }; } } } else { // We made it through all our source rows, we're done. break; } } // Finally, remove the old page. self.pages.remove(node); self.destroyPage(node); } /// This updates the cursor offset if the cursor is exactly on the cell /// we're currently reflowing. This can then be fixed up later to an exact /// x/y (see resizeCols). fn reflowUpdateCursor( self: *const PageList, cursor: ?*Resize.Cursor, src_cursor: *const ReflowCursor, dst_cursor: *const ReflowCursor, dst_node: *List.Node, ) void { // Update all our tracked pins var it = self.tracked_pins.keyIterator(); while (it.next()) |p_ptr| { const p = p_ptr.*; if (&p.page.data != src_cursor.page or p.y != src_cursor.y or p.x != src_cursor.x) continue; p.page = dst_node; p.x = dst_cursor.x; p.y = dst_cursor.y; } const c = cursor orelse return; // If our original cursor was on this page, this x/y then // we need to update to the new location. const offset = c.offset orelse return; if (&offset.page.data != src_cursor.page or offset.row_offset != src_cursor.y or c.x != src_cursor.x) return; // std.log.warn("c.x={} c.y={} dst_x={} dst_y={} src_y={}", .{ // c.x, // c.y, // dst_cursor.x, // dst_cursor.y, // src_cursor.y, // }); // Column always matches our dst x c.x = dst_cursor.x; // Our y is more complicated. The cursor y is the active // area y, not the row offset. Our cursors are row offsets. // Instead of calculating the active area coord, we can // better calculate the CHANGE in coordinate by subtracting // our dst from src which will calculate how many rows // we unwrapped to get here. // // Note this doesn't handle when we pull down scrollback. // See the cursor updates in resizeGrowCols for that. //c.y -|= src_cursor.y - dst_cursor.y; c.offset = .{ .page = dst_node, .row_offset = dst_cursor.y, }; } fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { if (opts.rows) |rows| { switch (std.math.order(rows, self.rows)) { .eq => {}, // Making rows smaller, we simply change our rows value. Changing // the row size doesn't affect anything else since max size and // so on are all byte-based. .lt => { // If our rows are shrinking, we prefer to trim trailing // blank lines from the active area instead of creating // history if we can. // // This matches macOS Terminal.app behavior. I chose to match that // behavior because it seemed fine in an ocean of differing behavior // between terminal apps. I'm completely open to changing it as long // as resize behavior isn't regressed in a user-hostile way. const trimmed = self.trimTrailingBlankRows(self.rows - rows); // If we have a cursor, we want to preserve the y value as // best we can. We need to subtract the number of rows that // moved into the scrollback. if (opts.cursor) |cursor| { const scrollback = self.rows - rows - trimmed; cursor.y -|= scrollback; } // If we didn't trim enough, just modify our row count and this // will create additional history. self.rows = rows; }, // Making rows larger we adjust our row count, and then grow // to the row count. .gt => gt: { // If our rows increased and our cursor is NOT at the bottom, // we want to try to preserve the y value of the old cursor. // In other words, we don't want to "pull down" scrollback. // This is purely a UX feature. if (opts.cursor) |cursor| cursor: { if (cursor.y >= self.rows - 1) break :cursor; // Cursor is not at the bottom, so we just grow our // rows and we're done. Cursor does NOT change for this // since we're not pulling down scrollback. for (0..rows - self.rows) |_| _ = try self.grow(); self.rows = rows; break :gt; } // Cursor is at the bottom or we don't care about cursors. // In this case, if we have enough rows in our pages, we // just update our rows and we're done. This effectively // "pulls down" scrollback. // // If we don't have enough scrollback, we add the difference, // to the active area. var count: usize = 0; var page = self.pages.first; while (page) |p| : (page = p.next) { count += p.data.size.rows; if (count >= rows) break; } else { assert(count < rows); for (count..rows) |_| _ = try self.grow(); } // Update our cursor. W if (opts.cursor) |cursor| { const grow_len: size.CellCountInt = @intCast(rows -| count); cursor.y += rows - self.rows - grow_len; } self.rows = rows; }, } } if (opts.cols) |cols| { switch (std.math.order(cols, self.cols)) { .eq => {}, // Making our columns smaller. We always have space for this // in existing pages so we need to go through the pages, // resize the columns, and clear any cells that are beyond // the new size. .lt => { var it = self.pageIterator(.{ .screen = .{} }, null); while (it.next()) |chunk| { const page = &chunk.page.data; const rows = page.rows.ptr(page.memory); for (0..page.size.rows) |i| { const row = &rows[i]; page.clearCells(row, cols, self.cols); } page.size.cols = cols; } if (opts.cursor) |cursor| { // If our cursor is off the edge we trimmed, update to edge if (cursor.x >= cols) cursor.x = cols - 1; } self.cols = cols; }, // Make our columns larger. This is a bit more complicated because // pages may not have the capacity for this. If they don't have // the capacity we need to allocate a new page and copy the data. .gt => { const cap = try std_capacity.adjust(.{ .cols = cols }); var it = self.pageIterator(.{ .screen = .{} }, null); while (it.next()) |chunk| { try self.resizeWithoutReflowGrowCols(cap, chunk, opts.cursor); } self.cols = cols; }, } } } fn resizeWithoutReflowGrowCols( self: *PageList, cap: Capacity, chunk: PageIterator.Chunk, cursor: ?*Resize.Cursor, ) !void { assert(cap.cols > self.cols); const page = &chunk.page.data; // Update our col count const old_cols = self.cols; self.cols = cap.cols; errdefer self.cols = old_cols; // Unlikely fast path: we have capacity in the page. This // is only true if we resized to less cols earlier. if (page.capacity.cols >= cap.cols) { page.size.cols = cap.cols; return; } // Likely slow path: we don't have capacity, so we need // to allocate a page, and copy the old data into it. // On error, we need to undo all the pages we've added. const prev = chunk.page.prev; errdefer { var current = chunk.page.prev; while (current) |p| { if (current == prev) break; current = p.prev; self.pages.remove(p); self.destroyPage(p); } } // We need to loop because our col growth may force us // to split pages. var copied: usize = 0; while (copied < page.size.rows) { const new_page = try self.createPage(cap); // The length we can copy into the new page is at most the number // of rows in our cap. But if we can finish our source page we use that. const len = @min(cap.rows, page.size.rows - copied); new_page.data.size.rows = len; // The range of rows we're copying from the old page. const y_start = copied; const y_end = copied + len; try new_page.data.cloneFrom(page, y_start, y_end); copied += len; // Insert our new page self.pages.insertBefore(chunk.page, new_page); // If we have a cursor, we need to update the row offset if it // matches what we just copied. if (cursor) |c| cursor: { const offset = c.offset orelse break :cursor; if (offset.page == chunk.page and offset.row_offset >= y_start and offset.row_offset < y_end) { c.offset = .{ .page = new_page, .row_offset = offset.row_offset - y_start, }; } } } assert(copied == page.size.rows); // Remove the old page. // Deallocate the old page. self.pages.remove(chunk.page); self.destroyPage(chunk.page); } /// Returns the number of trailing blank lines, not to exceed max. Max /// is used to limit our traversal in the case of large scrollback. fn trailingBlankLines( self: *const PageList, max: size.CellCountInt, ) size.CellCountInt { var count: size.CellCountInt = 0; // Go through our pages backwards since we're counting trailing blanks. var it = self.pages.last; while (it) |page| : (it = page.prev) { const len = page.data.size.rows; const rows = page.data.rows.ptr(page.data.memory)[0..len]; for (0..len) |i| { const rev_i = len - i - 1; const cells = rows[rev_i].cells.ptr(page.data.memory)[0..page.data.size.cols]; // If the row has any text then we're done. if (pagepkg.Cell.hasTextAny(cells)) return count; // Inc count, if we're beyond max then we're done. count += 1; if (count >= max) return count; } } return count; } /// Trims up to max trailing blank rows from the pagelist and returns the /// number of rows trimmed. A blank row is any row with no text (but may /// have styling). fn trimTrailingBlankRows( self: *PageList, max: size.CellCountInt, ) size.CellCountInt { var trimmed: size.CellCountInt = 0; var it = self.pages.last; while (it) |page| : (it = page.prev) { const len = page.data.size.rows; const rows_slice = page.data.rows.ptr(page.data.memory)[0..len]; for (0..len) |i| { const rev_i = len - i - 1; const row = &rows_slice[rev_i]; const cells = row.cells.ptr(page.data.memory)[0..page.data.size.cols]; // If the row has any text then we're done. if (pagepkg.Cell.hasTextAny(cells)) return trimmed; // No text, we can trim this row. Because it has // no text we can also be sure it has no styling // so we don't need to worry about memory. page.data.size.rows -= 1; trimmed += 1; if (trimmed >= max) return trimmed; } } return trimmed; } /// Scroll options. pub const Scroll = union(enum) { /// Scroll to the active area. This is also sometimes referred to as /// the "bottom" of the screen. This makes it so that the end of the /// screen is fully visible since the active area is the bottom /// rows/cols of the screen. active, /// Scroll to the top of the screen, which is the farthest back in /// the scrollback history. top, /// Scroll up (negative) or down (positive) by the given number of /// rows. This is clamped to the "top" and "active" top left. delta_row: isize, }; /// Scroll the viewport. This will never create new scrollback, allocate /// pages, etc. This can only be used to move the viewport within the /// previously allocated pages. pub fn scroll(self: *PageList, behavior: Scroll) void { switch (behavior) { .active => self.viewport = .{ .active = {} }, .top => self.viewport = .{ .top = {} }, .delta_row => |n| { if (n == 0) return; const top = self.getTopLeft2(.viewport); const p: Pin = if (n < 0) switch (top.upOverflow(@intCast(-n))) { .offset => |v| v, .overflow => |v| v.end, } else switch (top.downOverflow(@intCast(n))) { .offset => |v| v, .overflow => |v| v.end, }; // If we are still within the active area, then we pin the // viewport to active. This isn't EXACTLY the same behavior as // other scrolling because normally when you scroll the viewport // is pinned to _that row_ even if new scrollback is created. // But in a terminal when you get to the bottom and back into the // active area, you usually expect that the viewport will now // follow the active area. if (self.pinIsActive(p)) { self.viewport = .{ .active = {} }; return; } // Pin is not active so we need to track it. self.viewport_pin.* = p; self.viewport = .{ .pin = {} }; }, } } /// Clear the screen by scrolling written contents up into the scrollback. /// This will not update the viewport. pub fn scrollClear(self: *PageList) !void { // Go through the active area backwards to find the first non-empty // row. We use this to determine how many rows to scroll up. const non_empty: usize = non_empty: { var page = self.pages.last.?; var n: usize = 0; while (true) { const rows: [*]Row = page.data.rows.ptr(page.data.memory); for (0..page.data.size.rows) |i| { const rev_i = page.data.size.rows - i - 1; const row = rows[rev_i]; const cells = row.cells.ptr(page.data.memory)[0..self.cols]; for (cells) |cell| { if (!cell.isEmpty()) break :non_empty self.rows - n; } n += 1; if (n > self.rows) break :non_empty 0; } page = page.prev orelse break :non_empty 0; } }; // Scroll for (0..non_empty) |_| _ = try self.grow(); } /// Grow the active area by exactly one row. /// /// This may allocate, but also may not if our current page has more /// capacity we can use. This will prune scrollback if necessary to /// adhere to max_size. /// /// This returns the newly allocated page node if there is one. pub fn grow(self: *PageList) !?*List.Node { const last = self.pages.last.?; if (last.data.capacity.rows > last.data.size.rows) { // Fast path: we have capacity in the last page. last.data.size.rows += 1; return null; } // Slower path: we have no space, we need to allocate a new page. // If allocation would exceed our max size, we prune the first page. // We don't need to reallocate because we can simply reuse that first // page. if (self.page_size + PagePool.item_size > self.max_size) { const layout = Page.layout(try std_capacity.adjust(.{ .cols = self.cols })); // Get our first page and reset it to prepare for reuse. const first = self.pages.popFirst().?; assert(first != last); const buf = first.data.memory; @memset(buf, 0); // Initialize our new page and reinsert it as the last first.data = Page.initBuf(OffsetBuf.init(buf), layout); first.data.size.rows = 1; self.pages.insertAfter(last, first); // In this case we do NOT need to update page_size because // we're reusing an existing page so nothing has changed. return first; } // We need to allocate a new memory buffer. const next_page = try self.createPage(try std_capacity.adjust(.{ .cols = self.cols })); // we don't errdefer this because we've added it to the linked // list and its fine to have dangling unused pages. self.pages.append(next_page); next_page.data.size.rows = 1; // Accounting self.page_size += PagePool.item_size; assert(self.page_size <= self.max_size); return next_page; } /// Create a new page node. This does not add it to the list and this /// does not do any memory size accounting with max_size/page_size. fn createPage(self: *PageList, cap: Capacity) !*List.Node { var page = try self.pool.nodes.create(); errdefer self.pool.nodes.destroy(page); const page_buf = try self.pool.pages.create(); errdefer self.pool.pages.destroy(page_buf); if (comptime std.debug.runtime_safety) @memset(page_buf, 0); page.* = .{ .data = Page.initBuf( OffsetBuf.init(page_buf), Page.layout(cap), ), }; page.data.size.rows = 0; return page; } /// Destroy the memory of the given page and return it to the pool. The /// page is assumed to already be removed from the linked list. fn destroyPage(self: *PageList, page: *List.Node) void { @memset(page.data.memory, 0); self.pool.pages.destroy(@ptrCast(page.data.memory.ptr)); self.pool.nodes.destroy(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 eraseRows( self: *PageList, tl_pt: point.Point, bl_pt: ?point.Point, ) void { // The count of rows that was erased. var erased: usize = 0; // A pageIterator 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.pageIterator(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); erased += chunk.page.data.size.rows; 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 + chunk.end]; 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. // 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; } } // Our new size is the amount we scrolled chunk.page.data.size.rows = @intCast(scroll_amount); erased += chunk.end; } // If we deleted active, we need to regrow because one of our invariants // is that we always have full active space. if (tl_pt == .active) { for (0..erased) |_| _ = self.grow() catch |err| { // If this fails its a pretty big issue actually... but I don't // want to turn this function into an error-returning function // because erasing active is so rare and even if it happens failing // is even more rare... log.err("failed to regrow active area after erase err={}", .{err}); return; }; } // If we have a pinned viewport, we need to adjust for active area. switch (self.viewport) { .active => {}, // For pin, we check if our pin is now in the active area and if so // we move our viewport back to the active area. .pin => if (self.pinIsActive(self.viewport_pin.*)) { self.viewport = .{ .active = {} }; }, // For top, we move back to active if our erasing moved our // top page into the active area. .top => if (self.pinIsActive(.{ .page = self.pages.first.? })) { self.viewport = .{ .active = {} }; }, } } /// Erase a single page, freeing all its resources. The page can be /// 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 { 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; } // Remove the page from the linked list self.pages.remove(page); self.destroyPage(page); } /// Returns the pin for the given point. The pin is NOT tracked so it /// is only valid as long as the pagelist isn't modified. pub fn pin(self: *const PageList, pt: point.Point) ?Pin { 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 { assert(p != self.viewport_pin); if (self.tracked_pins.remove(p)) { self.pool.pins.destroy(p); } } /// Returns the viewport for the given pin, prefering to pin to /// "active" if the pin is within the active area. fn pinIsActive(self: *const PageList, p: Pin) bool { // If the pin is in the active page, then we can quickly determine // if we're beyond the end. const active = self.getTopLeft2(.active); if (p.page == active.page) return p.y >= active.y; var page_ = active.page.next; while (page_) |page| { // This loop is pretty fast because the active area is // never that large so this is at most one, two pages for // reasonable terminals (including very large real world // ones). // A page forward in the active area is our page, so we're // definitely in the active area. if (page == p.page) return true; page_ = page.next; } return false; } /// Convert a pin to a point in the given context. If the pin can't fit /// within the given tag (i.e. its in the history but you requested active), /// then this will return null. /// /// Note that this can be a very expensive operation depending on the tag and /// the location of the pin. This works by traversing the linked list of pages /// in the tagged region. /// /// Therefore, this is recommended only very rarely. pub fn pointFromPin(self: *const PageList, tag: point.Tag, p: Pin) ?point.Point { const tl = self.getTopLeft2(tag); // Count our first page which is special because it may be partial. var coord: point.Point.Coordinate = .{ .x = p.x }; if (p.page == tl.page) { // If our top-left is after our y then we're outside the range. if (tl.y > p.y) return null; coord.y = p.y - tl.y; } else { coord.y += tl.page.data.size.rows - tl.y - 1; var page_ = tl.page.next; while (page_) |page| : (page_ = page.next) { if (page == p.page) { coord.y += p.y; break; } coord.y += page.data.size.rows; } else { // We never saw our page, meaning we're outside the range. return null; } } return switch (tag) { inline else => |comptime_tag| @unionInit( point.Point, @tagName(comptime_tag), coord, ), }; } /// Get the cell at the given point, or null if the cell does not /// exist or is out of bounds. /// /// Warning: this is slow and should not be used in performance critical paths pub fn getCell(self: *const PageList, pt: point.Point) ?Cell { const pt_pin = self.pin(pt) orelse return null; const rac = pt_pin.page.data.getRowAndCell(pt_pin.x, pt_pin.y); return .{ .page = pt_pin.page, .row = rac.row, .cell = rac.cell, .row_idx = pt_pin.y, .col_idx = pt_pin.x, }; } pub const RowIterator = struct { page_it: PageIterator, chunk: ?PageIterator.Chunk = null, offset: usize = 0, pub fn next(self: *RowIterator) ?RowOffset { const chunk = self.chunk orelse return null; const row: RowOffset = .{ .page = chunk.page, .row_offset = self.offset }; // Increase our offset in the chunk self.offset += 1; // If we are beyond the chunk end, we need to move to the next chunk. if (self.offset >= chunk.end) { self.chunk = self.page_it.next(); if (self.chunk) |c| self.offset = c.start; } return row; } }; /// Create an interator that can be used to iterate all the rows in /// a region of the screen from the given top-left. The tag of the /// top-left point will also determine the end of the iteration, /// so convert from one reference point to another to change the /// iteration bounds. pub fn rowIterator( self: *const PageList, tl_pt: point.Point, bl_pt: ?point.Point, ) RowIterator { var page_it = self.pageIterator(tl_pt, bl_pt); const chunk = page_it.next() orelse return .{ .page_it = page_it }; return .{ .page_it = page_it, .chunk = chunk, .offset = chunk.start }; } pub const PageIterator = struct { row: ?RowOffset = null, limit: Limit = .none, const Limit = union(enum) { none, count: usize, row: RowOffset, }; pub fn next(self: *PageIterator) ?Chunk { // Get our current row location const row = self.row orelse return null; return switch (self.limit) { .none => none: { // If we have no limit, then we consume this entire page. Our // next row is the next page. self.row = next: { const next_page = row.page.next orelse break :next null; break :next .{ .page = next_page }; }; break :none .{ .page = row.page, .start = row.row_offset, .end = row.page.data.size.rows, }; }, .count => |*limit| count: { assert(limit.* > 0); // should be handled already const len = @min(row.page.data.size.rows - row.row_offset, limit.*); if (len > limit.*) { self.row = row.forward(len); limit.* -= len; } else { self.row = null; } break :count .{ .page = row.page, .start = row.row_offset, .end = row.row_offset + len, }; }, .row => |limit_row| row: { // If this is not the same page as our limit then we // can consume the entire page. if (limit_row.page != row.page) { self.row = next: { const next_page = row.page.next orelse break :next null; break :next .{ .page = next_page }; }; break :row .{ .page = row.page, .start = row.row_offset, .end = row.page.data.size.rows, }; } // If this is the same page then we only consume up to // the limit row. self.row = null; if (row.row_offset > limit_row.row_offset) return null; break :row .{ .page = row.page, .start = row.row_offset, .end = limit_row.row_offset + 1, }; }, }; } pub const Chunk = struct { page: *List.Node, start: usize, end: usize, pub fn rows(self: Chunk) []Row { const rows_ptr = self.page.data.rows.ptr(self.page.data.memory); 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; } }; }; /// Return an iterator that iterates through the rows in the tagged area /// of the point. The iterator returns row "chunks", which are the largest /// contiguous set of rows in a single backing page for a given portion of /// the point region. /// /// This is a more efficient way to iterate through the data in a region, /// since you can do simple pointer math and so on. /// /// If bl_pt is non-null, iteration will stop at the bottom left point /// (inclusive). If bl_pt is null, the entire region specified by the point /// tag will be iterated over. tl_pt and bl_pt must be the same tag, and /// bl_pt must be greater than or equal to tl_pt. pub fn pageIterator( self: *const PageList, tl_pt: point.Point, bl_pt: ?point.Point, ) PageIterator { // TODO: bl_pt assertions const tl = self.getTopLeft(tl_pt); const limit: PageIterator.Limit = limit: { if (bl_pt) |pt| { const bl = self.getTopLeft(pt); break :limit .{ .row = bl.forward(pt.coord().y).? }; } break :limit switch (tl_pt) { // These always go to the end of the screen. .screen, .active => .{ .none = {} }, // Viewport always is rows long .viewport => .{ .count = self.rows }, // 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. .history => history: { const active_tl = self.getTopLeft(.active); const history_bot = active_tl.backward(1) orelse return .{ .row = null }; break :history .{ .row = history_bot }; }, }; }; return .{ .row = tl.forward(tl_pt.coord().y), .limit = limit }; } /// Get the top-left of the screen for the given tag. fn getTopLeft(self: *const PageList, tag: point.Tag) RowOffset { 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.getTopLeft(.active), .top => self.getTopLeft(.screen), .pin => .{ .page = self.viewport_pin.page, .row_offset = self.viewport_pin.y }, }, // 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, .row_offset = page.data.size.rows - rem, }; }, }; } /// 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), .pin => self.viewport_pin.*, }, // 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 /// and not a capacity or maximum. /// /// This is very slow, it traverses the full list of pages to count the /// rows, so it is not pub. This is only used for testing/debugging. fn totalRows(self: *const PageList) usize { var rows: usize = 0; var page = self.pages.first; while (page) |p| { rows += p.data.size.rows; page = p.next; } return rows; } /// Grow the number of rows available in the page list by n. /// This is only used for testing so it isn't optimized. fn growRows(self: *PageList, n: usize) !void { var page = self.pages.last.?; var n_rem: usize = n; if (page.data.size.rows < page.data.capacity.rows) { const add = @min(n_rem, page.data.capacity.rows - page.data.size.rows); page.data.size.rows += add; if (n_rem == add) return; n_rem -= add; } while (n_rem > 0) { page = (try self.grow()).?; const add = @min(n_rem, page.data.capacity.rows); page.data.size.rows = add; n_rem -= add; } } /// 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 /// 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 /// than the original y-coordinate. This struct represents that mapping. pub const RowOffset = struct { page: *List.Node, row_offset: usize = 0, pub fn eql(self: RowOffset, other: RowOffset) bool { return self.page == other.page and self.row_offset == other.row_offset; } pub fn rowAndCell(self: RowOffset, x: usize) struct { row: *pagepkg.Row, cell: *pagepkg.Cell, } { const rac = self.page.data.getRowAndCell(x, self.row_offset); return .{ .row = rac.row, .cell = rac.cell }; } /// Get the row at the given row index from this Topleft. This /// may require traversing into the next page if the row index /// is greater than the number of rows in this page. /// /// This will return null if the row index is out of bounds. pub fn forward(self: RowOffset, idx: usize) ?RowOffset { return switch (self.forwardOverflow(idx)) { .offset => |v| v, .overflow => null, }; } /// TODO: docs pub fn backward(self: RowOffset, idx: usize) ?RowOffset { return switch (self.backwardOverflow(idx)) { .offset => |v| v, .overflow => null, }; } /// Move the offset forward n rows. If the offset goes beyond the /// end of the screen, return the overflow amount. fn forwardOverflow(self: RowOffset, n: usize) union(enum) { offset: RowOffset, overflow: struct { end: RowOffset, remaining: usize, }, } { // Index fits within this page const rows = self.page.data.size.rows - (self.row_offset + 1); if (n <= rows) return .{ .offset = .{ .page = self.page, .row_offset = n + self.row_offset, } }; // 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, .row_offset = page.data.size.rows - 1 }, .remaining = n_left, } }; if (n_left <= page.data.size.rows) return .{ .offset = .{ .page = page, .row_offset = n_left - 1, } }; n_left -= page.data.size.rows; } } /// Move the offset backward n rows. If the offset goes beyond the /// start of the screen, return the overflow amount. fn backwardOverflow(self: RowOffset, n: usize) union(enum) { offset: RowOffset, overflow: struct { end: RowOffset, remaining: usize, }, } { // Index fits within this page if (n <= self.row_offset) return .{ .offset = .{ .page = self.page, .row_offset = self.row_offset - n, } }; // Need to traverse page links to find the page var page: *List.Node = self.page; var n_left: usize = n - self.row_offset; while (true) { page = page.prev orelse return .{ .overflow = .{ .end = .{ .page = page, .row_offset = 0 }, .remaining = n_left, } }; if (n_left <= page.data.size.rows) return .{ .offset = .{ .page = page, .row_offset = page.data.size.rows - n_left, } }; n_left -= page.data.size.rows; } } }; const Cell = struct { page: *List.Node, row: *pagepkg.Row, cell: *pagepkg.Cell, row_idx: usize, col_idx: usize, /// Get the cell style. /// /// Not meant for non-test usage since this is inefficient. pub fn style(self: Cell) stylepkg.Style { if (self.cell.style_id == stylepkg.default_id) return .{}; return self.page.data.styles.lookupId( self.page.data.memory, self.cell.style_id, ).?.*; } /// Gets the screen point for the given cell. /// /// This is REALLY expensive/slow so it isn't pub. This was built /// for debugging and tests. If you have a need for this outside of /// this file then consider a different approach and ask yourself very /// carefully if you really need this. pub fn screenPoint(self: Cell) point.Point { var y: usize = self.row_idx; var page = self.page; while (page.prev) |prev| { y += prev.data.size.rows; page = prev; } return .{ .screen = .{ .x = self.col_idx, .y = y, } }; } }; test "PageList" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); try testing.expect(s.viewport == .active); try testing.expect(s.pages.first != null); try testing.expectEqual(@as(usize, s.rows), s.totalRows()); // Active area should be the top try testing.expectEqual(Pin{ .page = s.pages.first.?, .y = 0, .x = 0, }, s.getTopLeft2(.active)); } test "PageList pointFromPin active no history" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); { try testing.expectEqual(point.Point{ .active = .{ .y = 0, .x = 0, }, }, s.pointFromPin(.active, .{ .page = s.pages.first.?, .y = 0, .x = 0, }).?); } { try testing.expectEqual(point.Point{ .active = .{ .y = 2, .x = 4, }, }, s.pointFromPin(.active, .{ .page = s.pages.first.?, .y = 2, .x = 4, }).?); } } test "PageList pointFromPin active with history" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); try s.growRows(30); { try testing.expectEqual(point.Point{ .active = .{ .y = 0, .x = 2, }, }, s.pointFromPin(.active, .{ .page = s.pages.first.?, .y = 30, .x = 2, }).?); } // In history, invalid { try testing.expect(s.pointFromPin(.active, .{ .page = s.pages.first.?, .y = 21, .x = 2, }) == null); } } test "PageList pointFromPin active from prior page" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); const page = &s.pages.last.?.data; for (0..page.capacity.rows * 5) |_| { _ = try s.grow(); } { try testing.expectEqual(point.Point{ .active = .{ .y = 0, .x = 2, }, }, s.pointFromPin(.active, .{ .page = s.pages.last.?, .y = 0, .x = 2, }).?); } // Prior page { try testing.expect(s.pointFromPin(.active, .{ .page = s.pages.first.?, .y = 0, .x = 0, }) == null); } } test "PageList active after grow" { 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()); try s.growRows(10); try testing.expectEqual(@as(usize, s.rows + 10), s.totalRows()); // Make sure all points make sense { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 10, } }, pt); } { const pt = s.getCell(.{ .screen = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 10, } }, pt); } } test "PageList scroll top" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 10, } }, pt); } s.scroll(.{ .top = {} }); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } s.scroll(.{ .active = {} }); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 20, } }, pt); } } test "PageList scroll delta row back" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 10, } }, pt); } s.scroll(.{ .delta_row = -1 }); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 9, } }, pt); } try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 9, } }, pt); } } test "PageList scroll delta row back overflow" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 10, } }, pt); } s.scroll(.{ .delta_row = -100 }); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } } test "PageList scroll delta row forward" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 10, } }, pt); } s.scroll(.{ .top = {} }); s.scroll(.{ .delta_row = 2 }); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, pt); } try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, pt); } } test "PageList scroll delta row forward into active" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); s.scroll(.{ .delta_row = 2 }); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } } test "PageList scroll delta row back without space preserves active" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); s.scroll(.{ .delta_row = -1 }); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } try testing.expect(s.viewport == .active); } test "PageList scroll clear" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); { const cell = s.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; cell.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'A' }, }; } { const cell = s.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?; cell.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'A' }, }; } try s.scrollClear(); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, pt); } } test "PageList grow fit in capacity" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); // So we know we're using capacity to grow const last = &s.pages.last.?.data; try testing.expect(last.size.rows < last.capacity.rows); // Grow try testing.expect(try s.grow() == null); { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 1, } }, pt); } } test "PageList grow allocate" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); // Grow to capacity const last_node = s.pages.last.?; const last = &s.pages.last.?.data; for (0..last.capacity.rows - last.size.rows) |_| { try testing.expect(try s.grow() == null); } // Grow, should allocate const new = (try s.grow()).?; try testing.expect(s.pages.last.? == new); try testing.expect(last_node.next.? == new); { const cell = s.getCell(.{ .active = .{ .y = s.rows - 1 } }).?; try testing.expect(cell.page == new); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = last.capacity.rows, } }, cell.screenPoint()); } } test "PageList grow prune scrollback" { const testing = std.testing; const alloc = testing.allocator; // Zero here forces minimum max size to effectively two pages. var s = try init(alloc, 80, 24, 0); defer s.deinit(); // Grow to capacity const page1_node = s.pages.last.?; const page1 = page1_node.data; for (0..page1.capacity.rows - page1.size.rows) |_| { try testing.expect(try s.grow() == null); } // Grow and allocate one more page. Then fill that page up. const page2_node = (try s.grow()).?; const page2 = page2_node.data; for (0..page2.capacity.rows - page2.size.rows) |_| { try testing.expect(try s.grow() == null); } // Get our page size const old_page_size = s.page_size; // Next should create a new page, but it should reuse our first // page since we're at max size. const new = (try s.grow()).?; try testing.expect(s.pages.last.? == new); try testing.expectEqual(s.page_size, old_page_size); // Our first should now be page2 and our last should be page1 try testing.expectEqual(page2_node, s.pages.first.?); try testing.expectEqual(page1_node, s.pages.last.?); } test "PageList pageIterator single page" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); // The viewport should be within a single page try testing.expect(s.pages.first.?.next == null); // Iterate the active area var it = s.pageIterator(.{ .active = .{} }, null); { const chunk = it.next().?; try testing.expect(chunk.page == s.pages.first.?); try testing.expectEqual(@as(usize, 0), chunk.start); try testing.expectEqual(@as(usize, s.rows), chunk.end); } // Should only have one chunk try testing.expect(it.next() == null); } test "PageList pageIterator two pages" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); // Grow to capacity const page1_node = s.pages.last.?; const page1 = page1_node.data; for (0..page1.capacity.rows - page1.size.rows) |_| { try testing.expect(try s.grow() == null); } try testing.expect(try s.grow() != null); // Iterate the active area var it = s.pageIterator(.{ .active = .{} }, null); { const chunk = it.next().?; try testing.expect(chunk.page == s.pages.first.?); const start = chunk.page.data.size.rows - s.rows + 1; try testing.expectEqual(start, chunk.start); try testing.expectEqual(chunk.page.data.size.rows, chunk.end); } { const chunk = it.next().?; try testing.expect(chunk.page == s.pages.last.?); const start: usize = 0; try testing.expectEqual(start, chunk.start); try testing.expectEqual(start + 1, chunk.end); } try testing.expect(it.next() == null); } test "PageList pageIterator history two pages" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); // Grow to capacity const page1_node = s.pages.last.?; const page1 = page1_node.data; for (0..page1.capacity.rows - page1.size.rows) |_| { try testing.expect(try s.grow() == null); } try testing.expect(try s.grow() != null); // Iterate the active area var it = s.pageIterator(.{ .history = .{} }, null); { const active_tl = s.getTopLeft(.active); const chunk = it.next().?; try testing.expect(chunk.page == s.pages.first.?); const start: usize = 0; try testing.expectEqual(start, chunk.start); try testing.expectEqual(active_tl.row_offset, chunk.end); } 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.eraseRows(.{ .history = .{} }, null); 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" { 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(); } // Move our viewport to the top s.scroll(.{ .delta_row = -@as(isize, @intCast(s.totalRows())) }); try testing.expect(s.viewport == .pin); try testing.expect(s.viewport_pin.page == s.pages.first.?); // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, null); try testing.expect(s.viewport == .active); } test "PageList erase resets viewport if inside erased page but not active" { 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(); } // Move our viewport to the top s.scroll(.{ .delta_row = -@as(isize, @intCast(s.totalRows())) }); try testing.expect(s.viewport == .pin); try testing.expect(s.viewport_pin.page == s.pages.first.?); // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, .{ .history = .{ .y = 2 } }); try testing.expect(s.viewport == .pin); try testing.expect(s.viewport_pin.page == s.pages.first.?); } test "PageList erase resets viewport to active if top is inside active" { 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(); } // Move our viewport to the top s.scroll(.{ .top = {} }); // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, null); try testing.expect(s.viewport == .active); } test "PageList erase active regrows automatically" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 80, 24, null); defer s.deinit(); try testing.expect(s.totalRows() == s.rows); 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()); } test "PageList resize (no reflow) more rows" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 3, 0); defer s.deinit(); try testing.expectEqual(@as(usize, 3), s.totalRows()); // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 0, .y = 2 } }).?); defer s.untrackPin(p); // Resize try s.resize(.{ .rows = 10, .reflow = false }); try testing.expectEqual(@as(usize, 10), s.rows); try testing.expectEqual(@as(usize, 10), s.totalRows()); // Our cursor should not move because we have no scrollback so // we just grew. try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 2, } }, s.pointFromPin(.active, p.*).?); { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } } test "PageList resize (no reflow) more rows with history" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 3, null); defer s.deinit(); try s.growRows(50); { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 50, } }, pt); } // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 0, .y = 2 } }).?); defer s.untrackPin(p); // Resize try s.resize(.{ .rows = 5, .reflow = false }); try testing.expectEqual(@as(usize, 5), s.rows); try testing.expectEqual(@as(usize, 53), s.totalRows()); // Our cursor should move since it's in the scrollback try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 4, } }, s.pointFromPin(.active, p.*).?); { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 48, } }, pt); } } test "PageList resize (no reflow) less rows" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 10, 0); defer s.deinit(); try testing.expectEqual(@as(usize, 10), s.totalRows()); // This is required for our writing below to work try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; // Write into all rows so we don't get trim behavior for (0..s.rows) |y| { const rac = page.getRowAndCell(0, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'A' }, }; } // Resize try s.resize(.{ .rows = 5, .reflow = false }); try testing.expectEqual(@as(usize, 5), s.rows); try testing.expectEqual(@as(usize, 10), s.totalRows()); { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 5, } }, pt); } } test "PageList resize (no reflow) less rows cursor in scrollback" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 10, 0); defer s.deinit(); try testing.expectEqual(@as(usize, 10), s.totalRows()); // This is required for our writing below to work try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; // Write into all rows so we don't get trim behavior for (0..s.rows) |y| { const rac = page.getRowAndCell(0, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(y) }, }; } // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 0, .y = 2 } }).?); defer s.untrackPin(p); { const cursor = s.pointFromPin(.active, p.*).?.active; const get = s.getCell(.{ .active = .{ .x = cursor.x, .y = cursor.y, } }).?; try testing.expectEqual(@as(u21, 2), get.cell.content.codepoint); } // Resize try s.resize(.{ .rows = 5, .reflow = false }); try testing.expectEqual(@as(usize, 5), s.rows); try testing.expectEqual(@as(usize, 10), s.totalRows()); // Our cursor should move since it's in the scrollback try testing.expect(s.pointFromPin(.active, p.*) == null); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, s.pointFromPin(.screen, p.*).?); { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 5, } }, pt); } } test "PageList resize (no reflow) less rows trims blank lines" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 5, 0); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; // Write codepoint into first line { const rac = page.getRowAndCell(0, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'A' }, }; } // Fill remaining lines with a background color for (1..s.rows) |y| { const rac = page.getRowAndCell(0, y); rac.cell.* = .{ .content_tag = .bg_color_rgb, .content = .{ .color_rgb = .{ .r = 0xFF, .g = 0, .b = 0 } }, }; } // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 0, .y = 0 } }).?); defer s.untrackPin(p); { const cursor = s.pointFromPin(.active, p.*).?.active; const get = s.getCell(.{ .active = .{ .x = cursor.x, .y = cursor.y, } }).?; try testing.expectEqual(@as(u21, 'A'), get.cell.content.codepoint); } // Resize try s.resize(.{ .rows = 2, .reflow = false }); try testing.expectEqual(@as(usize, 2), s.rows); try testing.expectEqual(@as(usize, 2), s.totalRows()); // Our cursor should not move since we trimmed try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 0, } }, s.pointFromPin(.active, p.*).?); { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } } test "PageList resize (no reflow) more rows extends blank lines" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 3, 0); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; // Write codepoint into first line { const rac = page.getRowAndCell(0, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'A' }, }; } // Fill remaining lines with a background color for (1..s.rows) |y| { const rac = page.getRowAndCell(0, y); rac.cell.* = .{ .content_tag = .bg_color_rgb, .content = .{ .color_rgb = .{ .r = 0xFF, .g = 0, .b = 0 } }, }; } // Resize try s.resize(.{ .rows = 7, .reflow = false }); try testing.expectEqual(@as(usize, 7), s.rows); try testing.expectEqual(@as(usize, 7), s.totalRows()); { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } } test "PageList resize (no reflow) less cols" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 10, 0); defer s.deinit(); // Resize try s.resize(.{ .cols = 5, .reflow = false }); try testing.expectEqual(@as(usize, 5), s.cols); try testing.expectEqual(@as(usize, 10), s.totalRows()); var it = s.rowIterator(.{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); } } test "PageList resize (no reflow) less cols clears graphemes" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 10, 0); defer s.deinit(); // Add a grapheme. const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(9, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'A' }, }; try page.appendGrapheme(rac.row, rac.cell, 'A'); } try testing.expectEqual(@as(usize, 1), page.graphemeCount()); // Resize try s.resize(.{ .cols = 5, .reflow = false }); try testing.expectEqual(@as(usize, 5), s.cols); try testing.expectEqual(@as(usize, 10), s.totalRows()); var it = s.pageIterator(.{ .screen = .{} }, null); while (it.next()) |chunk| { try testing.expectEqual(@as(usize, 0), chunk.page.data.graphemeCount()); } } test "PageList resize (no reflow) more cols" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 5, 3, 0); defer s.deinit(); // Resize try s.resize(.{ .cols = 10, .reflow = false }); try testing.expectEqual(@as(usize, 10), s.cols); try testing.expectEqual(@as(usize, 3), s.totalRows()); var it = s.rowIterator(.{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expectEqual(@as(usize, 10), cells.len); } } test "PageList resize (no reflow) less cols then more cols" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 5, 3, 0); defer s.deinit(); // Resize less try s.resize(.{ .cols = 2, .reflow = false }); try testing.expectEqual(@as(usize, 2), s.cols); // Resize try s.resize(.{ .cols = 5, .reflow = false }); try testing.expectEqual(@as(usize, 5), s.cols); try testing.expectEqual(@as(usize, 3), s.totalRows()); var it = s.rowIterator(.{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); } } test "PageList resize (no reflow) less rows and cols" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 10, 0); defer s.deinit(); // Resize less try s.resize(.{ .cols = 5, .rows = 7, .reflow = false }); try testing.expectEqual(@as(usize, 5), s.cols); try testing.expectEqual(@as(usize, 7), s.rows); var it = s.rowIterator(.{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); } } test "PageList resize (no reflow) more rows and less cols" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 10, 0); defer s.deinit(); // Resize less try s.resize(.{ .cols = 5, .rows = 20, .reflow = false }); try testing.expectEqual(@as(usize, 5), s.cols); try testing.expectEqual(@as(usize, 20), s.rows); try testing.expectEqual(@as(usize, 20), s.totalRows()); var it = s.rowIterator(.{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); } } test "PageList resize (no reflow) empty screen" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 5, 5, 0); defer s.deinit(); // Resize try s.resize(.{ .cols = 10, .rows = 10, .reflow = false }); try testing.expectEqual(@as(usize, 10), s.cols); try testing.expectEqual(@as(usize, 10), s.rows); try testing.expectEqual(@as(usize, 10), s.totalRows()); var it = s.rowIterator(.{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expectEqual(@as(usize, 10), cells.len); } } test "PageList resize (no reflow) more cols forces smaller cap" { const testing = std.testing; const alloc = testing.allocator; // We want a cap that forces us to have less rows const cap = try std_capacity.adjust(.{ .cols = 100 }); const cap2 = try std_capacity.adjust(.{ .cols = 500 }); try testing.expectEqual(@as(size.CellCountInt, 500), cap2.cols); try testing.expect(cap2.rows < cap.rows); // Create initial cap, fits in one page var s = try init(alloc, cap.cols, cap.rows, null); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'A' }, }; } } // Resize to our large cap const rows = s.totalRows(); try s.resize(.{ .cols = cap2.cols, .reflow = false }); // Our total rows should be the same, and contents should be the same. try testing.expectEqual(rows, s.totalRows()); var it = s.rowIterator(.{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expectEqual(@as(usize, cap2.cols), cells.len); try testing.expectEqual(@as(u21, 'A'), cells[0].content.codepoint); } } test "PageList resize (no reflow) more rows adds blank rows if cursor at bottom" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 5, 3, null); defer s.deinit(); // Grow to 5 total rows, simulating 3 active + 2 scrollback try s.growRows(2); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.totalRows()) |y| { const rac = page.getRowAndCell(0, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(y) }, }; } // Active should be on row 3 { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, pt); } // Let's say our cursor is at the bottom var cursor: Resize.Cursor = .{ .x = 0, .y = s.rows - 2 }; // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 0, .y = s.rows - 2 } }).?); defer s.untrackPin(p); const original_cursor = s.pointFromPin(.active, p.*).?.active; { const get = s.getCell(.{ .active = .{ .x = original_cursor.x, .y = original_cursor.y, } }).?; try testing.expectEqual(@as(u21, 3), get.cell.content.codepoint); } // Resize try s.resizeWithoutReflow(.{ .rows = 10, .reflow = false, .cursor = &cursor }); try testing.expectEqual(@as(usize, 5), s.cols); try testing.expectEqual(@as(usize, 10), s.rows); // Our cursor should not change try testing.expectEqual(original_cursor, s.pointFromPin(.active, p.*).?.active); // 12 because we have our 10 rows in the active + 2 in the scrollback // because we're preserving the cursor. try testing.expectEqual(@as(usize, 12), s.totalRows()); // Active should be at the same place it was. { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, pt); } // Go through our active, we should get only 3,4,5 for (0..3) |y| { const get = s.getCell(.{ .active = .{ .y = y } }).?; const expected: u21 = @intCast(y + 2); try testing.expectEqual(expected, get.cell.content.codepoint); } } test "PageList resize reflow more cols no wrapped rows" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 5, 3, 0); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'A' }, }; } } // Resize try s.resize(.{ .cols = 10, .reflow = true }); try testing.expectEqual(@as(usize, 10), s.cols); try testing.expectEqual(@as(usize, 3), s.totalRows()); var it = s.rowIterator(.{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expectEqual(@as(usize, 10), cells.len); try testing.expectEqual(@as(u21, 'A'), cells[0].content.codepoint); } } test "PageList resize reflow more cols wrapped rows" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 4, 0); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { if (y % 2 == 0) { const rac = page.getRowAndCell(0, y); rac.row.wrap = true; } else { const rac = page.getRowAndCell(0, y); rac.row.wrap_continuation = true; } for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'A' }, }; } } // Resize try s.resize(.{ .cols = 4, .reflow = true }); try testing.expectEqual(@as(usize, 4), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); // Active should still be on top { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, pt); } var it = s.rowIterator(.{ .screen = .{} }, null); { // First row should be unwrapped const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 4), cells.len); try testing.expectEqual(@as(u21, 'A'), cells[0].content.codepoint); try testing.expectEqual(@as(u21, 'A'), cells[2].content.codepoint); } } test "PageList resize reflow more cols cursor in wrapped row" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 4, 0); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { { const rac = page.getRowAndCell(0, 0); rac.row.wrap = true; } for (0..s.cols) |x| { const rac = page.getRowAndCell(x, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } { { const rac = page.getRowAndCell(0, 1); rac.row.wrap_continuation = true; } for (0..s.cols) |x| { const rac = page.getRowAndCell(x, 1); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 1, .y = 1 } }).?); defer s.untrackPin(p); // Resize try s.resize(.{ .cols = 4, .reflow = true }); try testing.expectEqual(@as(usize, 4), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); // Our cursor should move to the first row try testing.expectEqual(point.Point{ .active = .{ .x = 3, .y = 0, } }, s.pointFromPin(.active, p.*).?); } test "PageList resize reflow more cols cursor in not wrapped row" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 4, 0); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { { const rac = page.getRowAndCell(0, 0); rac.row.wrap = true; } for (0..s.cols) |x| { const rac = page.getRowAndCell(x, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } { { const rac = page.getRowAndCell(0, 1); rac.row.wrap_continuation = true; } for (0..s.cols) |x| { const rac = page.getRowAndCell(x, 1); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 1, .y = 0 } }).?); defer s.untrackPin(p); // Resize try s.resize(.{ .cols = 4, .reflow = true }); try testing.expectEqual(@as(usize, 4), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); // Our cursor should move to the first row try testing.expectEqual(point.Point{ .active = .{ .x = 1, .y = 0, } }, s.pointFromPin(.active, p.*).?); } test "PageList resize reflow more cols cursor in wrapped row that isn't unwrapped" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 4, 0); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { { const rac = page.getRowAndCell(0, 0); rac.row.wrap = true; } for (0..s.cols) |x| { const rac = page.getRowAndCell(x, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } { { const rac = page.getRowAndCell(0, 1); rac.row.wrap = true; rac.row.wrap_continuation = true; } for (0..s.cols) |x| { const rac = page.getRowAndCell(x, 1); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } { { const rac = page.getRowAndCell(0, 2); rac.row.wrap_continuation = true; } for (0..s.cols) |x| { const rac = page.getRowAndCell(x, 2); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 1, .y = 2 } }).?); defer s.untrackPin(p); // Resize try s.resize(.{ .cols = 4, .reflow = true }); try testing.expectEqual(@as(usize, 4), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); // Our cursor should move to the first row try testing.expectEqual(point.Point{ .active = .{ .x = 1, .y = 1, } }, s.pointFromPin(.active, p.*).?); } test "PageList resize reflow more cols no reflow preserves semantic prompt" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 4, 0); defer s.deinit(); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; const rac = page.getRowAndCell(0, 1); rac.row.semantic_prompt = .prompt; } // Resize try s.resize(.{ .cols = 4, .reflow = true }); try testing.expectEqual(@as(usize, 4), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; const rac = page.getRowAndCell(0, 1); try testing.expect(rac.row.semantic_prompt == .prompt); } } test "PageList resize reflow more cols unwrap wide spacer head" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 2, 0); defer s.deinit(); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(0, 0); rac.row.wrap = true; rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'x' }, }; } { const rac = page.getRowAndCell(1, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = ' ' }, .wide = .spacer_head, }; } { const rac = page.getRowAndCell(0, 1); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = '😀' }, .wide = .wide, }; } { const rac = page.getRowAndCell(1, 1); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = ' ' }, .wide = .spacer_tail, }; } } // Resize try s.resize(.{ .cols = 4, .reflow = true }); try testing.expectEqual(@as(usize, 4), s.cols); try testing.expectEqual(@as(usize, 2), s.totalRows()); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(0, 0); try testing.expectEqual(@as(u21, 'x'), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.narrow, rac.cell.wide); try testing.expect(!rac.row.wrap); } { const rac = page.getRowAndCell(1, 0); try testing.expectEqual(@as(u21, '😀'), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.wide, rac.cell.wide); } { const rac = page.getRowAndCell(2, 0); try testing.expectEqual(@as(u21, ' '), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.spacer_tail, rac.cell.wide); } } } test "PageList resize reflow more cols unwrap still requires wide spacer head" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 2, 0); defer s.deinit(); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(0, 0); rac.row.wrap = true; rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'x' }, }; } { const rac = page.getRowAndCell(1, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'x' }, }; } { const rac = page.getRowAndCell(0, 1); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = '😀' }, .wide = .wide, }; } { const rac = page.getRowAndCell(1, 1); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = ' ' }, .wide = .spacer_tail, }; } } // Resize try s.resize(.{ .cols = 3, .reflow = true }); try testing.expectEqual(@as(usize, 3), s.cols); try testing.expectEqual(@as(usize, 2), s.totalRows()); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(0, 0); try testing.expectEqual(@as(u21, 'x'), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.narrow, rac.cell.wide); try testing.expect(rac.row.wrap); } { const rac = page.getRowAndCell(1, 0); try testing.expectEqual(@as(u21, 'x'), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.narrow, rac.cell.wide); } { const rac = page.getRowAndCell(2, 0); try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.spacer_head, rac.cell.wide); } { const rac = page.getRowAndCell(0, 1); try testing.expectEqual(@as(u21, '😀'), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.wide, rac.cell.wide); } { const rac = page.getRowAndCell(1, 1); try testing.expectEqual(@as(u21, ' '), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.spacer_tail, rac.cell.wide); } } } test "PageList resize reflow less cols no reflow preserves semantic prompt" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 4, 0); defer s.deinit(); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(0, 1); rac.row.semantic_prompt = .prompt; } for (0..s.cols) |x| { const rac = page.getRowAndCell(x, 1); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(0, 1); try testing.expect(rac.row.wrap); try testing.expect(rac.row.semantic_prompt == .prompt); } { const rac = page.getRowAndCell(0, 2); try testing.expect(rac.row.semantic_prompt == .prompt); } } } test "PageList resize reflow less cols no reflow preserves semantic prompt on first line" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 4, 0); defer s.deinit(); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; const rac = page.getRowAndCell(0, 0); rac.row.semantic_prompt = .prompt; } // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; const rac = page.getRowAndCell(0, 0); try testing.expect(rac.row.semantic_prompt == .prompt); } } test "PageList resize reflow less cols wrap preserves semantic prompt" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 4, 0); defer s.deinit(); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; const rac = page.getRowAndCell(0, 0); rac.row.semantic_prompt = .prompt; } // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; const rac = page.getRowAndCell(0, 0); try testing.expect(rac.row.semantic_prompt == .prompt); } } test "PageList resize reflow less cols no wrapped rows" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 3, 0); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { const end = 4; assert(end < s.cols); for (0..4) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Resize try s.resize(.{ .cols = 5, .reflow = true }); try testing.expectEqual(@as(usize, 5), s.cols); try testing.expectEqual(@as(usize, 3), s.totalRows()); var it = s.rowIterator(.{ .screen = .{} }, null); while (it.next()) |offset| { for (0..4) |x| { const rac = offset.rowAndCell(x); const cells = offset.page.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); try testing.expectEqual(@as(u21, @intCast(x)), cells[x].content.codepoint); } } } test "PageList resize reflow less cols wrapped rows" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 2, null); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); // Active moves due to scrollback { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, pt); } var it = s.rowIterator(.{ .screen = .{} }, null); { // First row should be wrapped const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); } { const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 2), cells[0].content.codepoint); } { // First row should be wrapped const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); } { const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 2), cells[0].content.codepoint); } } test "PageList resize reflow less cols wrapped rows with graphemes" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 2, null); defer s.deinit(); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } const rac = page.getRowAndCell(2, y); try page.appendGrapheme(rac.row, rac.cell, 'A'); } } // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); // Active moves due to scrollback { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, pt); } try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; var it = s.rowIterator(.{ .screen = .{} }, null); { // First row should be wrapped const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); } { const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 2), cells[0].content.codepoint); const cps = page.lookupGrapheme(rac.cell).?; try testing.expectEqual(@as(usize, 1), cps.len); try testing.expectEqual(@as(u21, 'A'), cps[0]); } { // First row should be wrapped const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); } { const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 2), cells[0].content.codepoint); const cps = page.lookupGrapheme(rac.cell).?; try testing.expectEqual(@as(usize, 1), cps.len); try testing.expectEqual(@as(u21, 'A'), cps[0]); } } test "PageList resize reflow less cols cursor in wrapped row" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 2, null); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 2, .y = 1 } }).?); defer s.untrackPin(p); // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); // Our cursor should move to the first row try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 1, } }, s.pointFromPin(.active, p.*).?); } test "PageList resize reflow less cols cursor goes to scrollback" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 2, null); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 2, .y = 0 } }).?); defer s.untrackPin(p); // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); // Our cursor should move to the first row try testing.expect(s.pointFromPin(.active, p.*) == null); } test "PageList resize reflow less cols cursor in unchanged row" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 2, null); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { for (0..2) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 1, .y = 0 } }).?); defer s.untrackPin(p); // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 2), s.totalRows()); // Our cursor should move to the first row try testing.expectEqual(point.Point{ .active = .{ .x = 1, .y = 0, } }, s.pointFromPin(.active, p.*).?); } test "PageList resize reflow less cols cursor in blank cell" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 6, 2, null); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { for (0..2) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .active = .{ .x = 2, .y = 0 } }).?); defer s.untrackPin(p); // Resize try s.resize(.{ .cols = 4, .reflow = true }); try testing.expectEqual(@as(usize, 4), s.cols); try testing.expectEqual(@as(usize, 2), s.totalRows()); // Our cursor should not move try testing.expectEqual(point.Point{ .active = .{ .x = 2, .y = 0, } }, s.pointFromPin(.active, p.*).?); } test "PageList resize reflow less cols cursor in final blank cell" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 6, 2, null); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..s.rows) |y| { for (0..2) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Set our cursor to be in the final cell of our resized var cursor: Resize.Cursor = .{ .x = 3, .y = 0 }; // Resize try s.resize(.{ .cols = 4, .reflow = true, .cursor = &cursor }); try testing.expectEqual(@as(usize, 4), s.cols); try testing.expectEqual(@as(usize, 2), s.totalRows()); // Our cursor should move to the first row try testing.expectEqual(@as(size.CellCountInt, 3), cursor.x); try testing.expectEqual(@as(size.CellCountInt, 0), cursor.y); } test "PageList resize reflow less cols blank lines" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 3, 0); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; for (0..1) |y| { for (0..4) |x| { const rac = page.getRowAndCell(x, y); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 3), s.totalRows()); var it = s.rowIterator(.{ .active = .{} }, null); { // First row should be wrapped const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); } { const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 2), cells[0].content.codepoint); } } test "PageList resize reflow less cols blank lines between" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 3, 0); defer s.deinit(); try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { for (0..4) |x| { const rac = page.getRowAndCell(x, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } { for (0..4) |x| { const rac = page.getRowAndCell(x, 2); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, }; } } // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 4), s.totalRows()); var it = s.rowIterator(.{ .active = .{} }, null); { const offset = it.next().?; const rac = offset.rowAndCell(0); try testing.expect(!rac.row.wrap); } { const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); } { const offset = it.next().?; const rac = offset.rowAndCell(0); const cells = offset.page.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 2), cells[0].content.codepoint); } } test "PageList resize reflow less cols copy style" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 4, 2, 0); defer s.deinit(); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; // Create a style const style: stylepkg.Style = .{ .flags = .{ .bold = true } }; const style_md = try page.styles.upsert(page.memory, style); for (0..s.cols - 1) |x| { const rac = page.getRowAndCell(x, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = @intCast(x) }, .style_id = style_md.id, }; style_md.ref += 1; } } // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 2), s.totalRows()); var it = s.rowIterator(.{ .active = .{} }, null); while (it.next()) |offset| { for (0..s.cols - 1) |x| { const rac = offset.rowAndCell(x); const style_id = rac.cell.style_id; try testing.expect(style_id != 0); const style = offset.page.data.styles.lookupId( offset.page.data.memory, style_id, ).?; try testing.expect(style.flags.bold); } } } test "PageList resize reflow less cols to eliminate a wide char" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 1, 0); defer s.deinit(); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(0, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = '😀' }, .wide = .wide, }; } { const rac = page.getRowAndCell(1, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = ' ' }, .wide = .spacer_tail, }; } } // Resize try s.resize(.{ .cols = 1, .reflow = true }); try testing.expectEqual(@as(usize, 1), s.cols); try testing.expectEqual(@as(usize, 1), s.totalRows()); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(0, 0); try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.narrow, rac.cell.wide); } } } test "PageList resize reflow less cols to wrap a wide char" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 3, 1, 0); defer s.deinit(); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(0, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 'x' }, }; } { const rac = page.getRowAndCell(1, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = '😀' }, .wide = .wide, }; } { const rac = page.getRowAndCell(2, 0); rac.cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = ' ' }, .wide = .spacer_tail, }; } } // Resize try s.resize(.{ .cols = 2, .reflow = true }); try testing.expectEqual(@as(usize, 2), s.cols); try testing.expectEqual(@as(usize, 2), s.totalRows()); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; { const rac = page.getRowAndCell(0, 0); try testing.expectEqual(@as(u21, 'x'), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.narrow, rac.cell.wide); try testing.expect(rac.row.wrap); } { const rac = page.getRowAndCell(1, 0); try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.spacer_head, rac.cell.wide); } { const rac = page.getRowAndCell(0, 1); try testing.expectEqual(@as(u21, '😀'), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.wide, rac.cell.wide); } { const rac = page.getRowAndCell(1, 1); try testing.expectEqual(@as(u21, ' '), rac.cell.content.codepoint); try testing.expectEqual(pagepkg.Cell.Wide.spacer_tail, rac.cell.wide); } } }