From 3842ca9212cd11e96a6d6b85b168b84a0fabf6cb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 28 Feb 2024 09:35:56 -0800 Subject: [PATCH] terminal/new: screen scrolling tests --- src/terminal/Screen.zig | 4 + src/terminal/new/PageList.zig | 54 ++++++----- src/terminal/new/Screen.zig | 163 +++++++++++++++++++++++++++++++++- src/terminal/new/Terminal.zig | 2 +- 4 files changed, 194 insertions(+), 29 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 0e00ec2fc..2650098f1 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -3860,6 +3860,7 @@ test "Screen: scroll down from 0" { } } +// X test "Screen: scrollback" { const testing = std.testing; const alloc = testing.allocator; @@ -3958,6 +3959,7 @@ test "Screen: scrollback" { } } +// X test "Screen: scrollback with large delta" { const testing = std.testing; const alloc = testing.allocator; @@ -3987,6 +3989,7 @@ test "Screen: scrollback with large delta" { } } +// X test "Screen: scrollback empty" { const testing = std.testing; const alloc = testing.allocator; @@ -4004,6 +4007,7 @@ test "Screen: scrollback empty" { } } +// X test "Screen: scrollback doesn't move viewport if not at bottom" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/new/PageList.zig b/src/terminal/new/PageList.zig index c188c1a0c..9adab8e5b 100644 --- a/src/terminal/new/PageList.zig +++ b/src/terminal/new/PageList.zig @@ -363,10 +363,10 @@ pub fn eraseRows( // The count of rows that was erased. var erased: usize = 0; - // A rowChunkIterator iterates one page at a time from the back forward. + // 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.rowChunkIterator(tl_pt, bl_pt); + 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. @@ -484,15 +484,21 @@ pub fn getCell(self: *const PageList, pt: point.Point) ?Cell { } pub const RowIterator = struct { - row: ?RowOffset = null, - limit: ?usize = null, + page_it: PageIterator, + chunk: ?PageIterator.Chunk = null, + offset: usize = 0, pub fn next(self: *RowIterator) ?RowOffset { - const row = self.row orelse return null; - self.row = row.forward(1); - if (self.limit) |*limit| { - limit.* -= 1; - if (limit.* == 0) self.row = null; + 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; @@ -507,14 +513,14 @@ pub const RowIterator = struct { pub fn rowIterator( self: *const PageList, tl_pt: point.Point, + bl_pt: ?point.Point, ) RowIterator { - const tl = self.getTopLeft(tl_pt); - - // TODO: limits - return .{ .row = tl.forward(tl_pt.coord().y) }; + 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 RowChunkIterator = struct { +pub const PageIterator = struct { row: ?RowOffset = null, limit: Limit = .none, @@ -524,7 +530,7 @@ pub const RowChunkIterator = struct { row: RowOffset, }; - pub fn next(self: *RowChunkIterator) ?Chunk { + pub fn next(self: *PageIterator) ?Chunk { // Get our current row location const row = self.row orelse return null; @@ -619,15 +625,15 @@ pub const RowChunkIterator = struct { /// (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 rowChunkIterator( +pub fn pageIterator( self: *const PageList, tl_pt: point.Point, bl_pt: ?point.Point, -) RowChunkIterator { +) PageIterator { // TODO: bl_pt assertions const tl = self.getTopLeft(tl_pt); - const limit: RowChunkIterator.Limit = limit: { + const limit: PageIterator.Limit = limit: { if (bl_pt) |pt| { const bl = self.getTopLeft(pt); break :limit .{ .row = bl.forward(pt.coord().y).? }; @@ -1227,7 +1233,7 @@ test "PageList grow prune scrollback" { try testing.expectEqual(page1_node, s.pages.last.?); } -test "PageList rowChunkIterator single page" { +test "PageList pageIterator single page" { const testing = std.testing; const alloc = testing.allocator; @@ -1238,7 +1244,7 @@ test "PageList rowChunkIterator single page" { try testing.expect(s.pages.first.?.next == null); // Iterate the active area - var it = s.rowChunkIterator(.{ .active = .{} }, null); + var it = s.pageIterator(.{ .active = .{} }, null); { const chunk = it.next().?; try testing.expect(chunk.page == s.pages.first.?); @@ -1250,7 +1256,7 @@ test "PageList rowChunkIterator single page" { try testing.expect(it.next() == null); } -test "PageList rowChunkIterator two pages" { +test "PageList pageIterator two pages" { const testing = std.testing; const alloc = testing.allocator; @@ -1266,7 +1272,7 @@ test "PageList rowChunkIterator two pages" { try testing.expect(try s.grow() != null); // Iterate the active area - var it = s.rowChunkIterator(.{ .active = .{} }, null); + var it = s.pageIterator(.{ .active = .{} }, null); { const chunk = it.next().?; try testing.expect(chunk.page == s.pages.first.?); @@ -1284,7 +1290,7 @@ test "PageList rowChunkIterator two pages" { try testing.expect(it.next() == null); } -test "PageList rowChunkIterator history two pages" { +test "PageList pageIterator history two pages" { const testing = std.testing; const alloc = testing.allocator; @@ -1300,7 +1306,7 @@ test "PageList rowChunkIterator history two pages" { try testing.expect(try s.grow() != null); // Iterate the active area - var it = s.rowChunkIterator(.{ .history = .{} }, null); + var it = s.pageIterator(.{ .history = .{} }, null); { const active_tl = s.getTopLeft(.active); const chunk = it.next().?; diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index 434971bc1..293901cf0 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -412,7 +412,7 @@ pub fn clearRows( bl: ?point.Point, protected: bool, ) void { - var it = self.pages.rowChunkIterator(tl, bl); + var it = self.pages.pageIterator(tl, bl); while (it.next()) |chunk| { for (chunk.rows()) |*row| { const cells_offset = row.cells; @@ -682,7 +682,7 @@ pub fn dumpString( ) !void { var blank_rows: usize = 0; - var iter = self.pages.rowIterator(tl); + var iter = self.pages.rowIterator(tl, null); while (iter.next()) |row_offset| { const rac = row_offset.rowAndCell(0); const cells = cells: { @@ -1087,7 +1087,6 @@ test "Screen: scrolling" { // Scroll down, should still be bottom try s.cursorDownScroll(); { - // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); @@ -1107,7 +1106,6 @@ test "Screen: scrolling" { s.scroll(.{ .active = {} }); { - // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); @@ -1132,3 +1130,160 @@ test "Screen: scroll down from 0" { try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); } } + +test "Screen: scrollback various cases" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 3, 1); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); + try s.cursorDownScroll(); + + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("2EFGH\n3IJKL", contents); + } + + // Scrolling to the bottom + s.scroll(.{ .active = {} }); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("2EFGH\n3IJKL", contents); + } + + // Scrolling back should make it visible again + s.scroll(.{ .delta_row = -1 }); + try testing.expect(s.pages.viewport != .active); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); + } + + // Scrolling back again should do nothing + s.scroll(.{ .delta_row = -1 }); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); + } + + // Scrolling to the bottom + s.scroll(.{ .active = {} }); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("2EFGH\n3IJKL", contents); + } + + // Scrolling forward with no grow should do nothing + s.scroll(.{ .delta_row = 1 }); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("2EFGH\n3IJKL", contents); + } + + // Scrolling to the top should work + s.scroll(.{ .top = {} }); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); + } + + // Should be able to easily clear active area only + s.clearRows(.{ .active = .{} }, null, false); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD", contents); + } + + // Scrolling to the bottom + s.scroll(.{ .active = {} }); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("", contents); + } +} + +test "Screen: scrollback with multi-row delta" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 3, 3); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH\n6IJKL"); + + // Scroll to top + s.scroll(.{ .top = {} }); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); + } + + // Scroll down multiple + s.scroll(.{ .delta_row = 5 }); + try testing.expect(s.pages.viewport == .active); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); + } +} + +test "Screen: scrollback empty" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 3, 50); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); + s.scroll(.{ .delta_row = 1 }); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); + } +} + +test "Screen: scrollback doesn't move viewport if not at bottom" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 3, 3); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"); + + // First test: we scroll up by 1, so we're not at the bottom anymore. + s.scroll(.{ .delta_row = -1 }); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("2EFGH\n3IJKL\n4ABCD", contents); + } + + // Next, we scroll back down by 1, this grows the scrollback but we + // shouldn't move. + try s.cursorDownScroll(); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("2EFGH\n3IJKL\n4ABCD", contents); + } + + // Scroll again, this clears scrollback so we should move viewports + // but still see the same thing since our original view fits. + try s.cursorDownScroll(); + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("2EFGH\n3IJKL\n4ABCD", contents); + } +} diff --git a/src/terminal/new/Terminal.zig b/src/terminal/new/Terminal.zig index 306fd2762..ab9fd1a20 100644 --- a/src/terminal/new/Terminal.zig +++ b/src/terminal/new/Terminal.zig @@ -1839,7 +1839,7 @@ pub fn decaln(self: *Terminal) !void { self.eraseDisplay(.complete, false); // Fill with Es, does not move cursor. - var it = self.screen.pages.rowChunkIterator(.{ .active = .{} }, null); + var it = self.screen.pages.pageIterator(.{ .active = .{} }, null); while (it.next()) |chunk| { for (chunk.rows()) |*row| { const cells_multi: [*]Cell = row.cells.ptr(chunk.page.data.memory);