From 3d0ec16ad4b2006ff98e740b67eb32c4666cb71e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 3 Sep 2022 21:25:06 -0700 Subject: [PATCH] resize with shrinking cols --- src/terminal/Screen.zig | 277 ++++- src/terminal/ScreenOld.zig | 2248 ------------------------------------ 2 files changed, 275 insertions(+), 2250 deletions(-) delete mode 100644 src/terminal/ScreenOld.zig diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 98b4c9439..6bb8d2586 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1027,8 +1027,122 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void { } } - // TODO - try self.resizeWithoutReflow(rows, cols); + // If our rows got smaller, we trim the scrollback. We do this after + // handling cols growing so that we can save as many lines as we can. + // We do it before cols shrinking so we can save compute on that operation. + if (rows < self.rows) try self.resizeWithoutReflow(rows, cols); + + // If our cols got smaller, we have to reflow text. This is the worst + // possible case because we can't do any easy trick sto get reflow, + // we just have to iterate over the screen and "print", wrapping as + // needed. + if (cols < self.cols) { + var old = self.*; + errdefer self.* = old; + + // Allocate enough to store our screen plus history. + const buf_size = (self.rows + @maximum(self.history, self.max_scrollback)) * (cols + 1); + self.storage = try StorageBuf.init(self.alloc, buf_size); + errdefer self.storage.deinit(self.alloc); + defer old.storage.deinit(self.alloc); + + // Convert our cursor coordinates to screen coordinates because + // we may have to reflow the cursor if the line it is on is moved. + var cursor_pos = (point.Viewport{ + .x = old.cursor.x, + .y = old.cursor.y, + }).toScreen(&old); + + // Whether we need to move the cursor or not + var new_cursor: ?point.ScreenPoint = null; + + // Reset our variables because we're going to reprint the screen. + self.cols = cols; + self.viewport = 0; + self.history = 0; + + // Iterate over the screen since we need to check for reflow. + var iter = old.rowIterator(.screen); + var x: usize = 0; + var y: usize = 0; + while (iter.next()) |old_row| { + // Trim the row from the right so that we ignore all trailing + // empty chars and don't wrap them. + const trimmed_row = trim: { + var i: usize = old.cols; + while (i > 0) : (i -= 1) if (!old_row.getCell(i - 1).empty()) break; + break :trim old_row.storage[1 .. i + 1]; + }; + + // Copy all the cells into our row. + for (trimmed_row) |cell, i| { + // Soft wrap if we have to + if (x == self.cols) { + var row = self.getRow(.{ .active = y }); + row.setWrapped(true); + x = 0; + y += 1; + } + + // If our y is more than our rows, we need to scroll + if (y >= self.rows) { + try self.scroll(.{ .delta = 1 }); + y -= 1; + x = 0; + } + + // If our cursor is on this point, we need to move it. + if (cursor_pos.y == iter.value - 1 and + cursor_pos.x == i) + { + assert(new_cursor == null); + new_cursor = .{ .x = x, .y = self.viewport + y }; + } + + // Copy the old cell, unset the old wrap state + // log.warn("y={} x={} rows={}", .{ y, x, self.rows }); + var new_cell = self.getCellPtr(.active, y, x); + new_cell.* = cell.cell; + + // Next + x += 1; + } + + // If our cursor is on this line but not in a content area, + // then we just set it to be at the end. + if (cursor_pos.y == iter.value - 1 and + cursor_pos.x >= trimmed_row.len) + { + assert(new_cursor == null); + new_cursor = .{ + .x = @minimum(cursor_pos.x, self.cols - 1), + .y = self.viewport + y, + }; + } + + // If we aren't wrapping, then move to the next row + if (trimmed_row.len == 0 or + !old_row.header().wrap) + { + y += 1; + x = 0; + } + } + + // If we have a new cursor, we need to convert that to a viewport + // point and set it up. + if (new_cursor) |pos| { + const viewport_pos = pos.toViewport(self); + self.cursor.x = viewport_pos.x; + self.cursor.y = viewport_pos.y; + } else { + // TODO: why is this necessary? Without this, neovim will + // crash when we shrink the window to the smallest size. We + // never got a test case to cover this. + self.cursor.x = @minimum(self.cursor.x, self.cols - 1); + self.cursor.y = @minimum(self.cursor.y, self.rows - 1); + } + } } /// Writes a basic string into the screen for testing. Newlines (\n) separate @@ -2107,3 +2221,162 @@ test "Screen: resize less rows with populated scrollback" { try testing.expectEqualStrings(expected, contents); } } + +test "Screen: resize less cols no reflow" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 0); + defer s.deinit(); + const str = "1AB\n2EF\n3IJ"; + try s.testWriteString(str); + const cursor = s.cursor; + try s.resize(3, 3); + + // Cursor should not move + try testing.expectEqual(cursor, s.cursor); + + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } + { + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } +} + +test "Screen: resize less cols with reflow but row space" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 0); + defer s.deinit(); + const str = "1ABCD"; + try s.testWriteString(str); + + // Put our cursor on the end + s.cursor.x = 4; + s.cursor.y = 0; + try testing.expectEqual(@as(u32, 'D'), s.getCell(.active, s.cursor.y, s.cursor.x).char); + + try s.resize(3, 3); + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + const expected = "1AB\nCD"; + try testing.expectEqualStrings(expected, contents); + } + { + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + const expected = "1AB\nCD"; + try testing.expectEqualStrings(expected, contents); + } + + // Cursor should be on the last line + try testing.expectEqual(@as(usize, 1), s.cursor.x); + try testing.expectEqual(@as(usize, 1), s.cursor.y); +} + +test "Screen: resize less cols with reflow with trimmed rows" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 0); + defer s.deinit(); + const str = "3IJKL\n4ABCD\n5EFGH"; + try s.testWriteString(str); + try s.resize(3, 3); + + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + const expected = "CD\n5EF\nGH"; + try testing.expectEqualStrings(expected, contents); + } + { + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + const expected = "CD\n5EF\nGH"; + try testing.expectEqualStrings(expected, contents); + } +} + +test "Screen: resize less cols with reflow with trimmed rows and scrollback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 1); + defer s.deinit(); + const str = "3IJKL\n4ABCD\n5EFGH"; + try s.testWriteString(str); + try s.resize(3, 3); + + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + const expected = "CD\n5EF\nGH"; + try testing.expectEqualStrings(expected, contents); + } + { + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + const expected = "4AB\nCD\n5EF\nGH"; + try testing.expectEqualStrings(expected, contents); + } +} + +// This seems like it should work fine but for some reason in practice +// in the initial implementation I found this bug! This is a regression +// test for that. +test "Screen: resize more rows then shrink again" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 10); + defer s.deinit(); + const str = "1ABC"; + try s.testWriteString(str); + + // Grow + try s.resize(10, 5); + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } + { + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } + + // Shrink + try s.resize(3, 5); + { + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } + + // Grow again + try s.resize(10, 5); + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } + { + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } +} diff --git a/src/terminal/ScreenOld.zig b/src/terminal/ScreenOld.zig deleted file mode 100644 index 1a8890705..000000000 --- a/src/terminal/ScreenOld.zig +++ /dev/null @@ -1,2248 +0,0 @@ -//! Screen represents the internal storage for a terminal screen, including -//! scrollback. This is implemented as a single continuous ring buffer. -//! -//! Definitions: -//! -//! * Screen - The full screen (active + history). -//! * Active - The area that is the current edit-able screen (the -//! bottom of the scrollback). This is "edit-able" because it is -//! the only part that escape sequences such as set cursor position -//! actually affect. -//! * History - The area that contains the lines prior to the active -//! area. This is the scrollback area. Escape sequences can no longer -//! affect this area. -//! * Viewport - The area that is currently visible to the user. This -//! can be thought of as the current window into the screen. -//! -const Screen = @This(); - -// FUTURE: Today this is implemented as a single contiguous ring buffer. -// If we increase the scrollback, we perform a full memory copy. For small -// scrollback, this is pretty cheap. For large (or infinite) scrollback, -// this starts to get pretty nasty. We should change this in the future to -// use a segmented list or something similar. I want to keep all the visible -// area contiguous so its not a simple drop-in. We can take a look at this -// one day. - -const std = @import("std"); -const utf8proc = @import("utf8proc"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const color = @import("color.zig"); -const point = @import("point.zig"); -const Selection = @import("Selection.zig"); - -const log = std.log.scoped(.screen); - -/// A row is a set of cells. -pub const Row = []Cell; - -/// Cursor represents the cursor state. -pub const Cursor = struct { - // x, y where the cursor currently exists (0-indexed). This x/y is - // always the offset in the active area. - x: usize = 0, - y: usize = 0, - - // pen is the current cell styling to apply to new cells. - pen: Cell = .{ .char = 0 }, - - // The last column flag (LCF) used to do soft wrapping. - pending_wrap: bool = false, -}; - -/// Cell is a single cell within the screen. -pub const Cell = struct { - /// Each cell contains exactly one character. The character is UTF-32 - /// encoded (just the Unicode codepoint). - char: u32, - - /// Foreground and background color. null means to use the default. - fg: ?color.RGB = null, - bg: ?color.RGB = null, - - /// On/off attributes that can be set - attrs: packed struct { - bold: bool = false, - faint: bool = false, - underline: bool = false, - inverse: bool = false, - - /// If 1, this line is soft-wrapped. Only the last cell in a row - /// should have this set. The first cell of the next row is actually - /// part of this row in raw input. - wrap: bool = false, - - /// True if this is a wide character. This char takes up - /// two cells. The following cell ALWAYS is a space. - wide: bool = false, - - /// Notes that this only exists to be blank for a preceeding - /// wide character (tail) or following (head). - wide_spacer_tail: bool = false, - wide_spacer_head: bool = false, - } = .{}, - - /// True if the cell should be skipped for drawing - pub fn empty(self: Cell) bool { - return self.char == 0; - } - - test { - // We use this test to ensure we always get the right size of the attrs - const cell: Cell = .{ .char = 0 }; - _ = @bitCast(u8, cell.attrs); - try std.testing.expectEqual(1, @sizeOf(@TypeOf(cell.attrs))); - } - - test { - //log.warn("CELL={}", .{@sizeOf(Cell)}); - try std.testing.expectEqual(16, @sizeOf(Cell)); - } -}; - -pub const RowIterator = struct { - screen: *const Screen, - tag: RowIndexTag, - value: usize = 0, - - pub fn next(self: *RowIterator) ?Row { - if (self.value >= self.tag.maxLen(self.screen)) return null; - const idx = self.tag.index(self.value); - const res = self.screen.getRow(idx); - self.value += 1; - return res; - } -}; - -/// RowIndex represents a row within the screen. There are various meanings -/// of a row index and this union represents the available types. For example, -/// when talking about row "0" you may want the first row in the viewport, -/// the first row in the scrollback, or the first row in the active area. -/// -/// All row indexes are 0-indexed. -pub const RowIndex = union(RowIndexTag) { - /// The index is from the top of the screen. The screen includes all - /// the history. - screen: usize, - - /// The index is from the top of the viewport. Therefore, depending - /// on where the user has scrolled the viewport, "0" is different. - viewport: usize, - - /// The index is from the top of the active area. The active area is - /// always "rows" tall, and 0 is the top row. The active area is the - /// "edit-able" area where the terminal cursor is. - active: usize, - - /// The index is from the top of the history (scrollback) to just - /// prior to the active area. - history: usize, -}; - -/// The tags of RowIndex -pub const RowIndexTag = enum { - screen, - viewport, - active, - history, - - /// The max length for a given tag. This is a length, not an index, - /// so it is 1-indexed. If the value is zero, it means that this - /// section of the screen is empty or disabled. - pub fn maxLen(self: RowIndexTag, screen: *const Screen) usize { - return switch (self) { - // The max of the screen is "bottom" so that we don't read - // past the pre-allocated space. - .screen => screen.bottom, - .viewport => screen.rows, - .active => screen.rows, - .history => screen.bottomOffset(), - }; - } - - /// Construct a RowIndex from a tag. - pub fn index(self: RowIndexTag, value: usize) RowIndex { - return switch (self) { - .screen => .{ .screen = value }, - .viewport => .{ .viewport = value }, - .active => .{ .active = value }, - .history => .{ .history = value }, - }; - } -}; - -/// Each screen maintains its own cursor state. -cursor: Cursor = .{}, - -/// Saved cursor saved with DECSC (ESC 7). -saved_cursor: Cursor = .{}, - -/// The full list of rows, including any scrollback. -storage: []Cell, - -/// The top and bottom of the scroll area. The first visible row if the terminal -/// window were scrolled all the way to the top. The last visible row if the -/// terminal were scrolled all the way to the bottom. -top: usize, -bottom: usize, - -/// The offset of the visible area within the storage. This is from the -/// "top" field. So the actual index of the first row is -/// `storage[top + visible_offset]`. -visible_offset: usize, - -/// The maximum number of lines that are available in scrollback. This -/// is in addition to the number of visible rows. -max_scrollback: usize, - -/// The number of rows and columns in the visible space. -rows: usize, -cols: usize, - -/// Initialize a new screen. -pub fn init( - alloc: Allocator, - rows: usize, - cols: usize, - max_scrollback: usize, -) !Screen { - // Allocate enough storage to cover every row and column in the visible - // area. This wastes some up front memory but saves allocations later. - // TODO: dynamically allocate scrollback - const buf = try alloc.alloc(Cell, (rows + max_scrollback) * cols); - std.mem.set(Cell, buf, .{ .char = 0 }); - - return Screen{ - .cursor = .{}, - .storage = buf, - .top = 0, - .bottom = rows, - .visible_offset = 0, - .max_scrollback = max_scrollback, - .rows = rows, - .cols = cols, - }; -} - -pub fn deinit(self: *Screen, alloc: Allocator) void { - alloc.free(self.storage); - self.* = undefined; -} - -/// This returns true if the viewport is anchored at the bottom currently. -pub fn viewportIsBottom(self: Screen) bool { - return self.visible_offset == self.bottomOffset(); -} - -fn bottomOffset(self: Screen) usize { - return self.bottom - self.rows; -} - -/// Returns an iterator that can be used to iterate over all of the rows -/// from index zero of the given row index type. This can therefore iterate -/// from row 0 of the active area, history, viewport, etc. -pub fn rowIterator(self: *const Screen, tag: RowIndexTag) RowIterator { - return .{ .screen = self, .tag = tag }; -} - -/// Region gets the contiguous portions of memory that constitute an -/// entire region. This is an efficient way to clear regions, for example -/// since you can memcpy directly into it. -/// -/// This has two elements because internally we use a ring buffer and -/// so any region can be split into two if it crosses the ring buffer -/// boundary. -pub fn region(self: *const Screen, tag: RowIndexTag) [2][]Cell { - const max_len = tag.maxLen(self); - if (max_len == 0) { - // This region is disabled or empty - return .{ self.storage[0..0], self.storage[0..0] }; - } - - const top = self.rowIndex(tag.index(0)); - const bot = self.rowIndex(tag.index(max_len - 1)); - - // The bottom and top are available in one contiguous slice. - if (bot >= top) { - return .{ - self.storage[top .. bot + self.cols], - self.storage[0..0], // just so its a valid slice, but zero length - }; - } - - // The bottom and top are split into two slices, so we slice to the - // bottom of the storage, then from the top. - return .{ - self.storage[top..self.storage.len], - self.storage[0 .. bot + self.cols], - }; -} - -/// Get a single row in the active area by index (0-indexed). -pub fn getRow(self: Screen, idx: RowIndex) Row { - // Get the index of the first byte of the the row at index. - const real_idx = self.rowIndex(idx); - - // The storage is sliced to return exactly the number of columns. - return self.storage[real_idx .. real_idx + self.cols]; -} - -/// Get a single cell in the active area. row and col are 0-indexed. -pub fn getCell(self: Screen, row: usize, col: usize) *Cell { - assert(row < self.rows); - assert(col < self.cols); - const row_idx = self.rowIndex(.{ .active = row }); - return &self.storage[row_idx + col]; -} - -/// Returns the index for the given row (0-indexed) into the underlying -/// storage array. The row is 0-indexed from the top of the screen. -fn rowIndex(self: *const Screen, idx: RowIndex) usize { - const y = switch (idx) { - .screen => |y| y: { - assert(y < RowIndexTag.screen.maxLen(self)); - break :y y; - }, - - .viewport => |y| y: { - assert(y < RowIndexTag.viewport.maxLen(self)); - break :y y + self.visible_offset; - }, - - .active => |y| y: { - assert(y < RowIndexTag.active.maxLen(self)); - break :y self.bottomOffset() + y; - }, - - .history => |y| y: { - assert(y < RowIndexTag.history.maxLen(self)); - break :y y; - }, - }; - - const val = (self.top + y) * self.cols; - if (val < self.storage.len) return val; - return val - self.storage.len; -} - -/// Returns the total number of rows in the screen. -inline fn totalRows(self: Screen) usize { - return self.storage.len / self.cols; -} - -/// Scroll behaviors for the scroll function. -pub const Scroll = union(enum) { - /// Scroll to the top of the scroll buffer. The first line of the - /// viewport will be the top line of the scroll buffer. - top: void, - - /// Scroll to the bottom, where the last line of the viewport - /// will be the last line of the buffer. TODO: are we sure? - bottom: void, - - /// Scroll up (negative) or down (positive) some fixed amount. - /// Scrolling direction (up/down) describes the direction the viewport - /// moves, not the direction text moves. This is the colloquial way that - /// scrolling is described: "scroll the page down". - delta: isize, - - /// Same as delta but scrolling down will not grow the scrollback. - /// Scrolling down at the bottom will do nothing (similar to how - /// delta at the top does nothing). - delta_no_grow: isize, -}; - -/// Scroll the screen by the given behavior. Note that this will always -/// "move" the screen. It is up to the caller to determine if they actually -/// want to do that yet (i.e. are they writing to the end of the screen -/// or not). -pub fn scroll(self: *Screen, behavior: Scroll) void { - switch (behavior) { - // Setting viewport offset to zero makes row 0 be at self.top - // which is the top! - .top => self.visible_offset = 0, - - // Calc the bottom by going from top of scrollback (self.top) - // to the end of the storage, then subtract the number of visible - // rows. - .bottom => self.visible_offset = self.bottom - self.rows, - - // TODO: deltas greater than the entire scrollback - .delta => |delta| self.scrollDelta(delta, true), - .delta_no_grow => |delta| self.scrollDelta(delta, false), - } -} - -fn scrollDelta(self: *Screen, delta: isize, grow: bool) void { - // log.info("offsets before: top={} bottom={} visible={}", .{ - // self.top, - // self.bottom, - // self.visible_offset, - // }); - // defer { - // log.info("offsets after: top={} bottom={} visible={}", .{ - // self.top, - // self.bottom, - // self.visible_offset, - // }); - // } - - // If we're scrolling up, then we just subtract and we're done. - if (delta < 0) { - self.visible_offset -|= @intCast(usize, -delta); - return; - } - - // If we're scrolling down, we have more work to do beacuse we - // need to determine if we're overwriting our scrollback. - self.visible_offset +|= @intCast(usize, delta); - if (grow) { - self.bottom +|= @intCast(usize, delta); - } else { - // If we're not growing, then we want to ensure we don't scroll - // off the bottom. Calculate the number of rows we can see. If we - // can see less than the number of rows we have available, then scroll - // back a bit. - const visible_bottom = self.visible_offset + self.rows; - if (visible_bottom > self.bottom) { - self.visible_offset = self.bottom - self.rows; - - // We can also fast-track this case because we know we won't - // be overlapping at all so we can return immediately. - return; - } - } - - // TODO: can optimize scrollback = 0 - - // Determine if we need to clear rows. - assert(@mod(self.storage.len, self.cols) == 0); - const storage_rows = self.storage.len / self.cols; - const visible_zero = self.top + self.visible_offset; - const rows_overlapped = if (visible_zero >= storage_rows) overlap: { - // We're wrapping from the top of the visible area. In this - // scenario, we just check that we have enough space from - // our true visible top to zero. - const visible_top = visible_zero - storage_rows; - const rows_available = self.top - visible_top; - if (rows_available >= self.rows) return; - - // We overlap our missing rows - break :overlap self.rows - rows_available; - } else overlap: { - // First check: if we have enough space in the storage buffer - // FORWARD to accomodate all our rows, then we're fine. - const rows_forward = storage_rows - (self.top + self.visible_offset); - if (rows_forward >= self.rows) return; - - // Second check: if we have enough space PRIOR to zero when - // wrapped, then we're fine. - const rows_wrapped = self.rows - rows_forward; - if (rows_wrapped < self.top) return; - - // We need to clear the rows in the overlap and move the top - // of the scrollback buffer. - break :overlap rows_wrapped - self.top; - }; - - // If we are growing, then we clear the overlap and reset zero - if (grow) { - // Clear our overlap - const clear_start = self.top * self.cols; - const clear_end = clear_start + (rows_overlapped * self.cols); - std.mem.set(Cell, self.storage[clear_start..clear_end], .{ .char = 0 }); - - // Move to accomodate overlap. This deletes scrollback. - self.top = @mod(self.top + rows_overlapped, storage_rows); - - // The new bottom is right up against the new top since we're using - // the full buffer. The bottom is therefore the full size of the storage. - self.bottom = storage_rows; - } - - // Move back the number of overlapped - self.visible_offset -= rows_overlapped; -} - -/// Copy row at src to dst. -pub fn copyRow(self: *Screen, dst: usize, src: usize) void { - const src_row = self.getRow(.{ .active = src }); - const dst_row = self.getRow(.{ .active = dst }); - std.mem.copy(Cell, dst_row, src_row); -} - -/// Resize the screen. The rows or cols can be bigger or smaller. This -/// function can only be used to resize the viewport. The scrollback size -/// (in lines) can't be changed. But due to the resize, more or less scrollback -/// "space" becomes available due to the width of lines. -/// -/// Due to the internal representation of a screen, this usually involves a -/// significant amount of copying compared to any other operations. -/// -/// This will trim data if the size is getting smaller. This will reflow the -/// soft wrapped text. -pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { - defer { - assert(self.cursor.x < self.cols); - assert(self.cursor.y < self.rows); - assert(self.rows == rows); - assert(self.cols == cols); - } - - // If the rows increased, we alloc space for the new rows (w/ existing cols) - // and move the viewport such that the bottom is in view. - if (rows > self.rows) { - var storage = try alloc.alloc( - Cell, - (rows + self.max_scrollback) * self.cols, - ); - - // Copy our screen into the new storage area. Since we're growing - // rows, we know that the full buffer will fit so we copy it in - // order. - const reg = self.region(.screen); - std.mem.copy(Cell, storage, reg[0]); - std.mem.copy(Cell, storage[reg[0].len..], reg[1]); - std.mem.set(Cell, storage[reg[0].len + reg[1].len ..], .{ .char = 0 }); - - // Modify our storage, our lines have grown - alloc.free(self.storage); - self.storage = storage; - - // Fix our row count - self.rows = rows; - - // Store our visible offset so we can move our cursor accordingly. - const old_offset = self.visible_offset; - - // Top is now 0 because we reoriented the ring buffer to be ordered. - // Bottom must be at least "rows" since we always show at least that - // much in the viewport. - self.top = 0; - self.bottom = @maximum(rows, self.bottom); - self.scroll(.{ .bottom = {} }); - - // Move our cursor to account for the new rows. The old offset - // should always be bigger (or the same) than the new offset since - // we are adding rows. - self.cursor.y += old_offset - self.visible_offset; - } - - // If our columns increased, we alloc space for the new column width - // and go through each row and reflow if necessary. - if (cols > self.cols) { - var storage = try alloc.alloc( - Cell, - (self.rows + self.max_scrollback) * cols, - ); - std.mem.set(Cell, storage, .{ .char = 0 }); - - // Convert our cursor coordinates to screen coordinates because - // we may have to reflow the cursor if the line it is on is unwrapped. - const cursor_pos = (point.Viewport{ - .x = self.cursor.x, - .y = self.cursor.y, - }).toScreen(self); - - // Nothing can fail from this point forward (no "try" expressions) - // so replace our storage. We defer freeing the "old" value because - // we need to access the old screen to copy. - var old = self.*; - defer { - assert(old.storage.ptr != self.storage.ptr); - alloc.free(old.storage); - } - self.storage = storage; - self.cols = cols; - - // Whether we need to move the cursor or not - var new_cursor: ?point.ScreenPoint = null; - - // Iterate over the screen since we need to check for reflow. - var iter = old.rowIterator(.screen); - var y: usize = 0; - while (iter.next()) |row| { - // No matter what we copy this row - var new_row = self.getRow(.{ .screen = y }); - std.mem.copy(Cell, new_row, row); - - // We need to check if our cursor was on this line - // and in the part that WAS copied. If so, we need to move it. - if (cursor_pos.y == iter.value - 1) { - assert(new_cursor == null); // should only happen once - new_cursor = .{ .y = y, .x = cursor_pos.x }; - } - - // If no reflow, just keep going - if (!row[row.len - 1].attrs.wrap) { - y += 1; - continue; - } - - // We need to reflow. At this point things get a bit messy. - // The goal is to keep the messiness of reflow down here and - // only reloop when we're back to clean non-wrapped lines. - - // Mark the last element as not wrapped - new_row[row.len - 1].attrs.wrap = false; - - // We maintain an x coord so that we can set cursors properly - var x: usize = row.len; - new_row = new_row[x..]; - wrapping: while (iter.next()) |wrapped_row| { - // Trim the row from the right so that we ignore all trailing - // empty chars and don't wrap them. - const trimmed_row = trim: { - var i: usize = wrapped_row.len; - while (i > 0) : (i -= 1) if (!wrapped_row[i - 1].empty()) break; - break :trim wrapped_row[0..i]; - }; - - var wrapped_rem = trimmed_row; - while (wrapped_rem.len > 0) { - // If the wrapped row fits nicely... - if (wrapped_rem.len <= new_row.len) { - // Copy the row - std.mem.copy(Cell, new_row, wrapped_rem); - - // If our cursor is in this line, then we have to move it - // onto the new line because it got unwrapped. - if (cursor_pos.y == iter.value - 1 and new_cursor == null) { - new_cursor = .{ .y = y, .x = cursor_pos.x + x }; - } - - // If this row isn't also wrapped, we're done! - if (!wrapped_rem[wrapped_rem.len - 1].attrs.wrap) { - y += 1; - - // If we were able to copy the entire row then - // we shortened the screen by one. We need to reflect - // this in our viewport. - if (wrapped_rem.len == trimmed_row.len and - self.visible_offset > 0) - { - self.visible_offset -= 1; - self.bottom -= 1; - } - - break :wrapping; - } - - // Wrapped again! - new_row[wrapped_rem.len - 1].attrs.wrap = false; - new_row = new_row[wrapped_rem.len..]; - x += wrapped_rem.len; - break; - } - - // The row doesn't fit, meaning we have to soft-wrap the - // new row but probably at a diff boundary. - std.mem.copy(Cell, new_row, wrapped_rem[0..new_row.len]); - new_row[new_row.len - 1].attrs.wrap = true; - - // We still need to copy the remainder - wrapped_rem = wrapped_rem[new_row.len..]; - - // We need to check if our cursor was on this line - // and in the part that WAS copied. If so, we need to move it. - if (cursor_pos.y == iter.value - 1 and - cursor_pos.x < new_row.len) - { - assert(new_cursor == null); // should only happen once - new_cursor = .{ .y = y, .x = x + cursor_pos.x }; - } - - // Move to a new line in our new screen - y += 1; - x = 0; - new_row = self.getRow(.{ .screen = y }); - } - } - } - - // If we have a new cursor, we need to convert that to a viewport - // point and set it up. - if (new_cursor) |pos| { - const viewport_pos = pos.toViewport(self); - self.cursor.x = viewport_pos.x; - self.cursor.y = viewport_pos.y; - } - } - - // If our rows got smaller, we trim the scrollback. - if (rows < self.rows) { - var storage = try alloc.alloc( - Cell, - (rows + self.max_scrollback) * self.cols, - ); - - // Get the slices for our full screen. We only copy the end of it - // that fits into our new memory region. We know we have the same - // number of columns in this block so we can just copy as-is. - const reg = self.region(.screen); - - // Trim the empty space off the end. The "end" might go into - // "top" since bottom may be empty or only implies the wraparound - // on the ring buffer. - const top = reg[0]; - const bot = reg[1]; - const bot_trimmed = trim: { - var i: usize = bot.len; - while (i > 0) : (i -= 1) if (!bot[i - 1].empty()) break; - i += self.cols - @mod(i, self.cols); - i = @minimum(bot.len, i); - break :trim bot[0..i]; - }; - const top_trimmed = if (bot.len > 0 and bot_trimmed.len == bot.len) noop: { - // We do nothing here because it means that we hit real content - // in the "bottom" so we don't want to trim zeros off the top - // when they might actually be useful. - break :noop top; - } else trim: { - var i: usize = top.len; - while (i > 0) : (i -= 1) if (!top[i - 1].empty()) break; - i += self.cols - @mod(i, self.cols); - i = @minimum(top.len, i); - break :trim top[0..i]; - }; - - // The trimmed also have to be cleanly divisible by rows since - // the copy and other math below depends on this invariant. - assert(@mod(bot_trimmed.len, self.cols) == 0); - assert(@mod(top_trimmed.len, self.cols) == 0); - - // Copy the top and bottom into the storage - const bot_len = @minimum(bot_trimmed.len, storage.len); - const top_len = @minimum(top_trimmed.len, storage.len - bot_len); - std.mem.copy(Cell, storage, top_trimmed[top_trimmed.len - top_len ..]); - std.mem.copy(Cell, storage[top_len..], bot_trimmed[bot_trimmed.len - bot_len ..]); - std.mem.set(Cell, storage[top_len + bot_len ..], .{ .char = 0 }); - - // Calculate the number of rows we copied since this will be - // our new "bottom". This should always divide cleanly because - // our cols haven't changed. - assert(@mod(top_len + bot_len, self.cols) == 0); - const copied_rows = (top_len + bot_len) / self.cols; - - // Modify our storage - alloc.free(self.storage); - self.storage = storage; - - // If our cursor was past the end of our old value, we pull it back. - if (self.cursor.y >= rows) { - self.cursor.y -= self.rows - rows; - } - - // Fix our row count - self.rows = rows; - - // Top is now 0 because we reoriented the ring buffer to be ordered. - // Bottom must be at least "rows" since we always show at least that - // much in the viewport. - self.top = 0; - self.bottom = @maximum(rows, copied_rows); - //log.warn("bot={} top={} copied={}", .{ bot_len, top_len, copied_rows }); - //log.warn("BOTTOM={}", .{self.bottom}); - self.scroll(.{ .bottom = {} }); - } - - // If our cols got smaller, we have to reflow text. This is the worst - // possible case because we can't do any easy trick sto get reflow, - // we just have to iterate over the screen and "print", wrapping as - // needed. - if (cols < self.cols) { - var storage = try alloc.alloc( - Cell, - (self.rows + self.max_scrollback) * cols, - ); - std.mem.set(Cell, storage, .{ .char = 0 }); - - // Convert our cursor coordinates to screen coordinates because - // we may have to reflow the cursor if the line it is on is moved. - var cursor_pos = (point.Viewport{ - .x = self.cursor.x, - .y = self.cursor.y, - }).toScreen(self); - - // Nothing can fail from this point forward (no "try" expressions) - // so replace our storage. We defer freeing the "old" value because - // we need to access the old screen to copy. - var old = self.*; - defer { - assert(old.storage.ptr != self.storage.ptr); - alloc.free(old.storage); - } - self.storage = storage; - self.cols = cols; - - // Whether we need to move the cursor or not - var new_cursor: ?point.ScreenPoint = null; - - // Iterate over the screen since we need to check for reflow. - var iter = old.rowIterator(.screen); - var x: usize = 0; - var y: usize = 0; - while (iter.next()) |row| { - // Trim the row from the right so that we ignore all trailing - // empty chars and don't wrap them. - const trimmed_row = trim: { - var i: usize = row.len; - while (i > 0) { - if (!row[i - 1].empty()) break; - i -= 1; - } - - break :trim row[0..i]; - }; - - // Copy all the cells into our row. - for (trimmed_row) |cell, i| { - // Soft wrap if we have to - if (x == self.cols) { - var last_cell = self.getCell(y, x - 1); - last_cell.attrs.wrap = true; - x = 0; - y += 1; - } - - // If our y is more than our rows, we need to scroll - if (y >= self.rows) { - self.scroll(.{ .delta = 1 }); - y = self.rows - 1; - x = 0; - } - - // If our cursor is on this point, we need to move it. - if (cursor_pos.y == iter.value - 1 and - cursor_pos.x == i) - { - assert(new_cursor == null); - new_cursor = .{ .x = x, .y = self.visible_offset + y }; - } - - // Copy the old cell, unset the old wrap state - // log.warn("y={} x={} rows={}", .{ y, x, self.rows }); - var new_cell = self.getCell(y, x); - new_cell.* = cell; - new_cell.attrs.wrap = false; - - // Next - x += 1; - } - - // If our cursor is on this line but not in a content area, - // then we just set it to be at the end. - if (cursor_pos.y == iter.value - 1 and - cursor_pos.x >= trimmed_row.len) - { - assert(new_cursor == null); - new_cursor = .{ - .x = @minimum(cursor_pos.x, self.cols - 1), - .y = self.visible_offset + y, - }; - } - - // If we aren't wrapping, then move to the next row - if (trimmed_row.len == 0 or - !trimmed_row[trimmed_row.len - 1].attrs.wrap) - { - y += 1; - x = 0; - } - } - - // If we have a new cursor, we need to convert that to a viewport - // point and set it up. - if (new_cursor) |pos| { - const viewport_pos = pos.toViewport(self); - self.cursor.x = viewport_pos.x; - self.cursor.y = viewport_pos.y; - } else { - // TODO: why is this necessary? Without this, neovim will - // crash when we shrink the window to the smallest size - self.cursor.x = @minimum(self.cursor.x, self.cols - 1); - self.cursor.y = @minimum(self.cursor.y, self.rows - 1); - } - } -} - -/// Resize the screen without any reflow. In this mode, columns/rows will -/// be truncated as they are shrunk. If they are grown, the new space is filled -/// with zeros. -pub fn resizeWithoutReflow(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { - // Resize without reflow not supported for now with scrollback. - assert(self.max_scrollback == 0); - - // Make a copy so we can access the old indexes. - const old = self.*; - - // Reallocate the storage - self.storage = try alloc.alloc(Cell, (rows + self.max_scrollback) * cols); - defer alloc.free(old.storage); - std.mem.set(Cell, self.storage, .{ .char = 0 }); - self.top = 0; - self.bottom = rows; - self.rows = rows; - self.cols = cols; - - // Move our cursor if we have to so it stays on the screen. - self.cursor.x = @minimum(self.cursor.x, self.cols - 1); - self.cursor.y = @minimum(self.cursor.y, self.rows - 1); - - // If we're increasing height, then copy all rows (start at 0). - // Otherwise start at the latest row that includes the bottom row, - // aka strip the top. - var y: usize = if (rows >= old.rows) 0 else old.rows - rows; - const start = y; - const col_end = @minimum(old.cols, cols); - while (y < old.rows) : (y += 1) { - // Copy the old row into the new row, just losing the columsn - // if we got thinner. - const old_row = old.getRow(.{ .viewport = y }); - const new_row = self.getRow(.{ .viewport = y - start }); - std.mem.copy(Cell, new_row, old_row[0..col_end]); - - // If our new row is wider, then we copy zeroes into the rest. - if (new_row.len > old_row.len) { - std.mem.set(Cell, new_row[old_row.len..], .{ .char = 0 }); - } - } - - // If we grew rows, then set the remaining data to zero. - if (rows > old.rows) { - std.mem.set(Cell, self.storage[self.rowIndex(.{ .viewport = old.rows })..], .{ .char = 0 }); - } -} - -/// Returns the raw text associated with a selection. This will unwrap -/// soft-wrapped edges. The returned slice is owned by the caller. -pub fn selectionString(self: Screen, alloc: Allocator, sel: Selection) ![:0]const u8 { - // Get the slices for the string - const slices = self.selectionSlices(sel); - - // We can now know how much space we'll need to store the string. We loop - // over and UTF8-encode and calculate the exact size required. We will be - // off here by at most "newlines" values in the worst case that every - // single line is soft-wrapped. - const newlines = @divFloor(slices.top.len + slices.bot.len, self.cols) + 1; - const chars = chars: { - var count: usize = 0; - const arr = [_][]Cell{ slices.top, slices.bot }; - for (arr) |slice| { - for (slice) |cell| { - var buf: [4]u8 = undefined; - const char = if (cell.char > 0) cell.char else ' '; - count += try std.unicode.utf8Encode(@intCast(u21, char), &buf); - } - } - - break :chars count; - }; - const buf = try alloc.alloc(u8, chars + newlines + 1); - errdefer alloc.free(buf); - - var i: usize = 0; - for (slices.top) |cell, idx| { - // If our index cleanly divides into the col count then we're - // at a newline and we add it. - if (idx > 0 and - @mod(idx + slices.top_offset, self.cols) == 0 and - !slices.top[idx - 1].attrs.wrap) - { - buf[i] = '\n'; - i += 1; - } - - // Skip spacers - if (cell.attrs.wide_spacer_head or - cell.attrs.wide_spacer_tail) continue; - - const char = if (cell.char > 0) cell.char else ' '; - i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]); - } - - for (slices.bot) |cell, idx| { - // We don't use "top_offset" here because the bot by definition - // is never offset, it always starts at index 0 so we can just check - // the index directly. - if (@mod(idx, self.cols) == 0) { - // Determine if we soft-wrapped. For the bottom slice this is - // a bit unique because if we're at idx 0, we actually need to - // check the end of the top. - const wrapped = if (idx > 0) - slices.bot[idx - 1].attrs.wrap - else - slices.top[slices.top.len - 1].attrs.wrap; - - if (!wrapped) { - buf[i] = '\n'; - i += 1; - } - } - - // Skip spacers - if (cell.attrs.wide_spacer_head or - cell.attrs.wide_spacer_tail) continue; - - const char = if (cell.char > 0) cell.char else ' '; - i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]); - } - - // Add null termination - buf[i] = 0; - - // Realloc so our free length is exactly correct - const result = try alloc.realloc(buf, i + 1); - return result[0..i :0]; -} - -/// Returns the slices that make up the selection, in order. There are at most -/// two parts to handle the ring buffer. If the selection fits in one contiguous -/// slice, then the second slice will have a length of zero. -fn selectionSlices(self: Screen, sel_raw: Selection) struct { - // Top offset can be used to determine if a newline is required by - // seeing if the cell index plus the offset cleanly divides by screen cols. - top_offset: usize, - top: []Cell, - bot: []Cell, -} { - // Note: this function is tested via selectionString - - assert(sel_raw.start.y < self.totalRows()); - assert(sel_raw.end.y < self.totalRows()); - assert(sel_raw.start.x < self.cols); - assert(sel_raw.end.x < self.cols); - - const sel = sel: { - var sel = sel_raw; - - // If the end of our selection is a wide char leader, include the - // first part of the next line. - if (sel.end.x == self.cols - 1) { - const row = self.getRow(.{ .screen = sel.end.y }); - if (row[sel.end.x].attrs.wide_spacer_head) { - sel.end.y += 1; - sel.end.x = 0; - } - } - - // If the start of our selection is a wide char spacer, include the - // wide char. - if (sel.start.x > 0) { - const row = self.getRow(.{ .screen = sel.start.y }); - if (row[sel.start.x].attrs.wide_spacer_tail) { - sel.end.x -= 1; - } - } - - break :sel sel; - }; - - // Get the true "top" and "bottom" - const sel_top = sel.topLeft(); - const sel_bot = sel.bottomRight(); - const top = self.rowIndex(.{ .screen = sel_top.y }); - const bot = self.rowIndex(.{ .screen = sel_bot.y }); - - // The bottom and top are available in one contiguous slice. - if (bot >= top) { - return .{ - .top_offset = sel_top.x, - .top = self.storage[top + sel_top.x .. bot + sel_bot.x + 1], - .bot = self.storage[0..0], // just so its a valid slice, but zero length - }; - } - - // The bottom and top are split into two slices, so we slice to the - // bottom of the storage, then from the top. - return .{ - .top_offset = sel_top.x, - .top = self.storage[top + sel_top.x .. self.storage.len], - .bot = self.storage[0 .. bot + sel_bot.x + 1], - }; -} - -/// Turns the screen into a string. Different regions of the screen can -/// be selected using the "tag", i.e. if you want to output the viewport, -/// the scrollback, the full screen, etc. -pub fn testString(self: Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8 { - const buf = try alloc.alloc(u8, self.storage.len + self.rows + 1); - - var i: usize = 0; - var y: usize = 0; - var rows = self.rowIterator(tag); - while (rows.next()) |row| { - defer y += 1; - - if (y > 0) { - buf[i] = '\n'; - i += 1; - } - - for (row) |cell| { - // TODO: handle character after null - if (cell.char > 0) { - i += try std.unicode.utf8Encode(@intCast(u21, cell.char), buf[i..]); - } - } - } - - // Never render the final newline - const str = std.mem.trimRight(u8, buf[0..i], "\n"); - return try alloc.realloc(buf, str.len); -} - -/// Writes a basic string into the screen for testing. Newlines (\n) separate -/// each row. If a line is longer than the available columns, soft-wrapping -/// will occur. -pub fn testWriteString(self: *Screen, text: []const u8) void { - var y: usize = 0; - var x: usize = 0; - - const view = std.unicode.Utf8View.init(text) catch unreachable; - var iter = view.iterator(); - while (iter.nextCodepoint()) |c| { - // Explicit newline forces a new row - if (c == '\n') { - y += 1; - x = 0; - continue; - } - - // If we're writing past the end of the active area, scroll. - if (y >= self.rows) { - y -= 1; - self.scroll(.{ .delta = 1 }); - } - - // Get our row - var row = self.getRow(.{ .active = y }); - - // If we're writing past the end, we need to soft wrap. - if (x == self.cols) { - row[x - 1].attrs.wrap = true; - y += 1; - x = 0; - if (y >= self.rows) { - y -= 1; - self.scroll(.{ .delta = 1 }); - } - row = self.getRow(.{ .active = y }); - } - - // If our character is double-width, handle it. - const width = utf8proc.charwidth(c); - assert(width == 1 or width == 2); - switch (width) { - 1 => row[x].char = @intCast(u32, c), - - 2 => { - if (x == self.cols - 1) { - row[x].char = ' '; - row[x].attrs.wide_spacer_head = true; - - // wrap - row[x].attrs.wrap = true; - y += 1; - x = 0; - if (y >= self.rows) { - y -= 1; - self.scroll(.{ .delta = 1 }); - } - row = self.getRow(.{ .active = y }); - } - - row[x].char = @intCast(u32, c); - row[x].attrs.wide = true; - - x += 1; - row[x].char = ' '; - row[x].attrs.wide_spacer_tail = true; - }, - - else => unreachable, - } - - x += 1; - } -} - -test "Screen" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - - // Sanity check that our test helpers work - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Test the row iterator - var count: usize = 0; - var iter = s.rowIterator(.viewport); - while (iter.next()) |row| { - // Rows should be pointer equivalent to getRow - const row_other = s.getRow(.{ .viewport = count }); - try testing.expectEqual(row.ptr, row_other.ptr); - count += 1; - } - - // Should go through all rows - try testing.expectEqual(@as(usize, 3), count); - - // Should be able to easily clear screen - const reg = s.region(.viewport); - std.mem.set(Cell, reg[0], .{ .char = 'A' }); - std.mem.set(Cell, reg[1], .{ .char = 'A' }); - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("AAAAA\nAAAAA\nAAAAA", contents); - } -} - -test "Screen: scrolling" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - - try testing.expect(s.viewportIsBottom()); - - // Scroll down, should still be bottom - s.scroll(.{ .delta = 1 }); - try testing.expect(s.viewportIsBottom()); - - // Test our row index - try testing.expectEqual(@as(usize, 5), s.rowIndex(.{ .active = 0 })); - try testing.expectEqual(@as(usize, 10), s.rowIndex(.{ .active = 1 })); - try testing.expectEqual(@as(usize, 0), s.rowIndex(.{ .active = 2 })); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling to the bottom does nothing - s.scroll(.{ .bottom = {} }); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } -} - -// TODO -// test "Screen: scrolling more than size" { -// const testing = std.testing; -// const alloc = testing.allocator; -// -// var s = try init(alloc, 3, 5, 3); -// defer s.deinit(alloc); -// s.testWriteString("1ABCD\n2EFGH\n3IJKL"); -// -// try testing.expect(s.viewportIsBottom()); -// -// // Scroll down, should still be bottom -// s.scroll(.{ .delta = 7 }); -// try testing.expect(s.viewportIsBottom()); -// -// // Test our row index -// try testing.expectEqual(@as(usize, 5), s.rowIndex(0)); -// try testing.expectEqual(@as(usize, 10), s.rowIndex(1)); -// try testing.expectEqual(@as(usize, 15), s.rowIndex(2)); -// } - -test "Screen: scroll down from 0" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - s.scroll(.{ .delta = -1 }); - try testing.expect(s.viewportIsBottom()); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } -} - -test "Screen: scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 1); - defer s.deinit(alloc); - s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - s.scroll(.{ .delta = 1 }); - - // Test our row index - try testing.expectEqual(@as(usize, 5), s.rowIndex(.{ .active = 0 })); - try testing.expectEqual(@as(usize, 10), s.rowIndex(.{ .active = 1 })); - try testing.expectEqual(@as(usize, 15), s.rowIndex(.{ .active = 2 })); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling to the bottom - s.scroll(.{ .bottom = {} }); - try testing.expect(s.viewportIsBottom()); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling back should make it visible again - s.scroll(.{ .delta = -1 }); - try testing.expect(!s.viewportIsBottom()); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - // Scrolling back again should do nothing - s.scroll(.{ .delta = -1 }); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - // Scrolling to the bottom - s.scroll(.{ .bottom = {} }); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling forward with no grow should do nothing - s.scroll(.{ .delta_no_grow = 1 }); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } - - // Scrolling to the top should work - s.scroll(.{ .top = {} }); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - // Should be able to easily clear active area only - const reg = s.region(.active); - std.mem.set(Cell, reg[0], .{ .char = 0 }); - std.mem.set(Cell, reg[1], .{ .char = 0 }); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD", contents); - } - - // Scrolling to the bottom - s.scroll(.{ .bottom = {} }); - - { - // Test our contents rotated - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("", contents); - } -} - -test "Screen: scrollback empty" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 50); - defer s.deinit(alloc); - s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - s.scroll(.{ .delta_no_grow = 1 }); - - { - // Test our contents - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } -} - -test "Screen: history region with scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 1, 5, 0); - defer s.deinit(alloc); - - // Write a bunch that WOULD invoke scrollback if exists - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Verify no scrollback - const reg = s.region(.history); - try testing.expect(reg[0].len == 0); - try testing.expect(reg[1].len == 0); -} - -test "Screen: history region with scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 1, 5, 2); - defer s.deinit(alloc); - - // Write a bunch that WOULD invoke scrollback if exists - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - { - // Test our contents - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); - } - - // Verify history region - const reg = s.region(.history); - try testing.expect(reg[0].len > 0); - try testing.expect(reg[1].len >= 0); - - { - var contents = try s.testString(alloc, .history); - defer alloc.free(contents); - const expected = "1ABCD\n2EFGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: row copy" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - - // Copy - s.scroll(.{ .delta = 1 }); - s.copyRow(2, 0); - - // Test our contents - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL\n2EFGH", contents); -} - -test "Screen: selectionString" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - - { - var contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 1 }, - .end = .{ .x = 2, .y = 2 }, - }); - defer alloc.free(contents); - const expected = "2EFGH\n3IJ"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: selectionString soft wrap" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD2EFGH3IJKL"; - s.testWriteString(str); - - { - var contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 1 }, - .end = .{ .x = 2, .y = 2 }, - }); - defer alloc.free(contents); - const expected = "2EFGH3IJ"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: selectionString wrap around" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - try testing.expect(s.viewportIsBottom()); - - // Scroll down, should still be bottom, but should wrap because - // we're out of space. - s.scroll(.{ .delta = 1 }); - try testing.expect(s.viewportIsBottom()); - try testing.expectEqual(@as(usize, 0), s.rowIndex(.{ .active = 2 })); - s.testWriteString("1ABCD\n2EFGH\n3IJKL"); - - { - var contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 1 }, - .end = .{ .x = 2, .y = 2 }, - }); - defer alloc.free(contents); - const expected = "2EFGH\n3IJ"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: selectionString wide char" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1A⚡"; - s.testWriteString(str); - - { - var contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 3, .y = 0 }, - }); - defer alloc.free(contents); - const expected = str; - try testing.expectEqualStrings(expected, contents); - } - - { - var contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 2, .y = 0 }, - }); - defer alloc.free(contents); - const expected = str; - try testing.expectEqualStrings(expected, contents); - } - - { - var contents = try s.selectionString(alloc, .{ - .start = .{ .x = 3, .y = 0 }, - .end = .{ .x = 3, .y = 0 }, - }); - defer alloc.free(contents); - const expected = "⚡"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: selectionString wide char with header" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABC⚡"; - s.testWriteString(str); - - { - var contents = try s.selectionString(alloc, .{ - .start = .{ .x = 0, .y = 0 }, - .end = .{ .x = 4, .y = 0 }, - }); - defer alloc.free(contents); - const expected = str; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: resize more rows no scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - const cursor = s.cursor; - try s.resize(alloc, 10, 5); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize more rows with empty scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 10); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - const cursor = s.cursor; - try s.resize(alloc, 10, 5); - try testing.expectEqual(@as(usize, 20), s.totalRows()); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize more rows with populated scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; - s.testWriteString(str); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } - - // Set our cursor to be on the "4" - s.cursor.x = 0; - s.cursor.y = 1; - try testing.expectEqual(@as(u32, '4'), s.getCell(s.cursor.y, s.cursor.x).char); - - // Resize - try s.resize(alloc, 10, 5); - try testing.expectEqual(@as(usize, 15), s.totalRows()); - - // Cursor should still be on the "4" - try testing.expectEqual(@as(u32, '4'), s.getCell(s.cursor.y, s.cursor.x).char); - // s.cursor.x = 0; - // s.cursor.y = 1; - //try testing.expectEqual(cursor, s.cursor); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize more cols no reflow" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - const cursor = s.cursor; - try s.resize(alloc, 3, 10); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize more cols with reflow that fits full width" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD2EFGH\n3IJKL"; - s.testWriteString(str); - - // Verify we soft wrapped - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD\n2EFGH\n3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Let's put our cursor on row 2, where the soft wrap is - s.cursor.x = 0; - s.cursor.y = 1; - try testing.expectEqual(@as(u32, '2'), s.getCell(s.cursor.y, s.cursor.x).char); - - // Resize and verify we undid the soft wrap because we have space now - try s.resize(alloc, 3, 10); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Our cursor should've moved - try testing.expectEqual(@as(usize, 5), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); -} - -test "Screen: resize more cols with reflow that ends in newline" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 6, 0); - defer s.deinit(alloc); - const str = "1ABCD2EFGH\n3IJKL"; - s.testWriteString(str); - - // Verify we soft wrapped - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD2\nEFGH\n3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Let's put our cursor on the last row - s.cursor.x = 0; - s.cursor.y = 2; - try testing.expectEqual(@as(u32, '3'), s.getCell(s.cursor.y, s.cursor.x).char); - - // Resize and verify we undid the soft wrap because we have space now - try s.resize(alloc, 3, 10); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Our cursor should still be on the 3 - try testing.expectEqual(@as(u32, '3'), s.getCell(s.cursor.y, s.cursor.x).char); -} - -test "Screen: resize more cols with reflow that forces more wrapping" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD2EFGH\n3IJKL"; - s.testWriteString(str); - - // Let's put our cursor on row 2, where the soft wrap is - s.cursor.x = 0; - s.cursor.y = 1; - try testing.expectEqual(@as(u32, '2'), s.getCell(s.cursor.y, s.cursor.x).char); - - // Verify we soft wrapped - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD\n2EFGH\n3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Resize and verify we undid the soft wrap because we have space now - try s.resize(alloc, 3, 7); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD2E\nFGH\n3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Our cursor should've moved - try testing.expectEqual(@as(usize, 5), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); -} - -test "Screen: resize more cols with reflow that unwraps multiple times" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD2EFGH3IJKL"; - s.testWriteString(str); - - // Let's put our cursor on row 2, where the soft wrap is - s.cursor.x = 0; - s.cursor.y = 2; - try testing.expectEqual(@as(u32, '3'), s.getCell(s.cursor.y, s.cursor.x).char); - - // Verify we soft wrapped - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD\n2EFGH\n3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Resize and verify we undid the soft wrap because we have space now - try s.resize(alloc, 3, 15); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABCD2EFGH3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - - // Our cursor should've moved - try testing.expectEqual(@as(usize, 10), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); -} - -test "Screen: resize more cols with populated scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD5EFGH"; - s.testWriteString(str); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } - - // // Set our cursor to be on the "5" - s.cursor.x = 0; - s.cursor.y = 2; - try testing.expectEqual(@as(u32, '5'), s.getCell(s.cursor.y, s.cursor.x).char); - - // Resize - try s.resize(alloc, 3, 10); - - // Cursor should still be on the "5" - log.warn("cursor={}", .{s.cursor}); - try testing.expectEqual(@as(u32, '5'), s.getCell(s.cursor.y, s.cursor.x).char); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "2EFGH\n3IJKL\n4ABCD5EFGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: resize less rows no scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - const cursor = s.cursor; - try s.resize(alloc, 1, 5); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: resize less rows moving cursor" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - - // Put our cursor on the last line - s.cursor.x = 1; - s.cursor.y = 2; - try testing.expectEqual(@as(u32, 'I'), s.getCell(s.cursor.y, s.cursor.x).char); - - // Resize - try s.resize(alloc, 1, 5); - - // Cursor should be on the last line - try testing.expectEqual(@as(usize, 1), s.cursor.x); - try testing.expectEqual(@as(usize, 0), s.cursor.y); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: resize less rows with empty scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 10); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - try s.resize(alloc, 1, 5); - - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: resize less rows with populated scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 5); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; - s.testWriteString(str); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "3IJKL\n4ABCD\n5EFGH"; - try testing.expectEqualStrings(expected, contents); - } - - // Resize - try s.resize(alloc, 1, 5); - - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "5EFGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: resize less cols no reflow" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1AB\n2EF\n3IJ"; - s.testWriteString(str); - const cursor = s.cursor; - try s.resize(alloc, 3, 3); - - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize less cols with reflow but row space" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD"; - s.testWriteString(str); - - // Put our cursor on the end - s.cursor.x = 4; - s.cursor.y = 0; - try testing.expectEqual(@as(u32, 'D'), s.getCell(s.cursor.y, s.cursor.x).char); - - try s.resize(alloc, 3, 3); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1AB\nCD"; - try testing.expectEqualStrings(expected, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "1AB\nCD"; - try testing.expectEqualStrings(expected, contents); - } - - // Cursor should be on the last line - try testing.expectEqual(@as(usize, 1), s.cursor.x); - try testing.expectEqual(@as(usize, 1), s.cursor.y); -} - -test "Screen: resize less cols with reflow with trimmed rows" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "3IJKL\n4ABCD\n5EFGH"; - s.testWriteString(str); - try s.resize(alloc, 3, 3); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "CD\n5EF\nGH"; - try testing.expectEqualStrings(expected, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "CD\n5EF\nGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -test "Screen: resize less cols with reflow with trimmed rows and scrollback" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 1); - defer s.deinit(alloc); - const str = "3IJKL\n4ABCD\n5EFGH"; - s.testWriteString(str); - try s.resize(alloc, 3, 3); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "CD\n5EF\nGH"; - try testing.expectEqualStrings(expected, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - const expected = "4AB\nCD\n5EF\nGH"; - try testing.expectEqualStrings(expected, contents); - } -} - -// This seems like it should work fine but for some reason in practice -// in the initial implementation I found this bug! This is a regression -// test for that. -test "Screen: resize more rows then shrink again" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 10); - defer s.deinit(alloc); - const str = "1ABC"; - s.testWriteString(str); - - // Grow - try s.resize(alloc, 10, 5); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Shrink - try s.resize(alloc, 3, 5); - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - - // Grow again - try s.resize(alloc, 10, 5); - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } - { - var contents = try s.testString(alloc, .screen); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize (no reflow) more rows" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - try s.resizeWithoutReflow(alloc, 10, 5); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize (no reflow) less rows" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - try s.resizeWithoutReflow(alloc, 2, 5); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } -} - -test "Screen: resize (no reflow) more cols" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - try s.resizeWithoutReflow(alloc, 3, 10); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize (no reflow) less cols" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 3, 5, 0); - defer s.deinit(alloc); - const str = "1ABCD\n2EFGH\n3IJKL"; - s.testWriteString(str); - try s.resizeWithoutReflow(alloc, 3, 4); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABC\n2EFG\n3IJK"; - try testing.expectEqualStrings(expected, contents); - } -}