diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 5ed7d70a1..0bdb7a285 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -5170,6 +5170,7 @@ test "Screen: promtpPath" { } } +// X - we don't use this in new terminal test "Screen: scrollRegionUp single" { const testing = std.testing; const alloc = testing.allocator; @@ -5187,6 +5188,7 @@ test "Screen: scrollRegionUp single" { } } +// X - we don't use this in new terminal test "Screen: scrollRegionUp same line" { const testing = std.testing; const alloc = testing.allocator; @@ -5204,6 +5206,7 @@ test "Screen: scrollRegionUp same line" { } } +// X - we don't use this in new terminal test "Screen: scrollRegionUp single with pen" { const testing = std.testing; const alloc = testing.allocator; @@ -5228,6 +5231,7 @@ test "Screen: scrollRegionUp single with pen" { } } +// X - we don't use this in new terminal test "Screen: scrollRegionUp multiple" { const testing = std.testing; const alloc = testing.allocator; @@ -5245,6 +5249,7 @@ test "Screen: scrollRegionUp multiple" { } } +// X - we don't use this in new terminal test "Screen: scrollRegionUp multiple count" { const testing = std.testing; const alloc = testing.allocator; @@ -5262,6 +5267,7 @@ test "Screen: scrollRegionUp multiple count" { } } +// X - we don't use this in new terminal test "Screen: scrollRegionUp count greater than available lines" { const testing = std.testing; const alloc = testing.allocator; @@ -5278,6 +5284,7 @@ test "Screen: scrollRegionUp count greater than available lines" { try testing.expectEqualStrings("1ABCD\n\n\n4ABCD", contents); } } +// X - we don't use this in new terminal test "Screen: scrollRegionUp fills with pen" { const testing = std.testing; const alloc = testing.allocator; @@ -5302,6 +5309,7 @@ test "Screen: scrollRegionUp fills with pen" { } } +// X - we don't use this in new terminal test "Screen: scrollRegionUp buffer wrap" { const testing = std.testing; const alloc = testing.allocator; @@ -5334,6 +5342,7 @@ test "Screen: scrollRegionUp buffer wrap" { } } +// X - we don't use this in new terminal test "Screen: scrollRegionUp buffer wrap alternate" { const testing = std.testing; const alloc = testing.allocator; @@ -5366,6 +5375,7 @@ test "Screen: scrollRegionUp buffer wrap alternate" { } } +// X - we don't use this in new terminal test "Screen: scrollRegionUp buffer wrap alternative with extra lines" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/new/PageList.zig b/src/terminal/new/PageList.zig index e6972f78c..edd68b657 100644 --- a/src/terminal/new/PageList.zig +++ b/src/terminal/new/PageList.zig @@ -319,6 +319,34 @@ pub fn clonePool( return result; } +/// Returns the viewport for the given offset, prefering to pin to +/// "active" if the offset is within the active area. +fn viewportForOffset(self: *const PageList, offset: RowOffset) Viewport { + // If the offset is on the active page, then we pin to active + // if our row idx is beyond the active row idx. + const active = self.getTopLeft(.active); + if (offset.page == active.page) { + if (offset.row_offset >= active.row_offset) { + return .{ .active = {} }; + } + } else { + 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 == offset.page) return .{ .active = {} }; + page_ = page.next; + } + } + + return .{ .exact = offset }; +} + /// Scroll options. pub const Scroll = union(enum) { /// Scroll to the active area. This is also sometimes referred to as @@ -343,7 +371,7 @@ pub fn scroll(self: *PageList, behavior: Scroll) void { switch (behavior) { .active => self.viewport = .{ .active = {} }, .top => self.viewport = .{ .top = {} }, - .delta_row => |n| delta_row: { + .delta_row => |n| { if (n == 0) return; const top = self.getTopLeft(.viewport); @@ -362,25 +390,7 @@ pub fn scroll(self: *PageList, behavior: Scroll) void { // 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. - const active = self.getTopLeft(.active); - if (offset.page == active.page) { - if (offset.row_offset >= active.row_offset) { - self.viewport = .{ .active = {} }; - break :delta_row; - } - } else active: { - // Check forward pages too. - var page = active.page.next orelse break :active; - while (true) { - if (page == offset.page) { - self.viewport = .{ .active = {} }; - break :delta_row; - } - page = page.next orelse break :active; - } - } - - self.viewport = .{ .exact = offset }; + self.viewport = self.viewportForOffset(offset); }, } } @@ -562,6 +572,23 @@ pub fn eraseRows( return; }; } + + // If we have an exact viewport, we need to adjust for active area. + switch (self.viewport) { + .active => {}, + + .exact => |offset| self.viewport = self.viewportForOffset(offset), + + // For top, we move back to active if our erasing moved our + // top page into the active area. + .top => { + const vp = self.viewportForOffset(.{ + .page = self.pages.first.?, + .row_offset = 0, + }); + if (vp == .active) self.viewport = vp; + }, + } } /// Erase a single page, freeing all its resources. The page can be @@ -1479,7 +1506,7 @@ test "PageList erase" { try testing.expectEqual(s.rows, s.totalRows()); } -test "PageList erase resets viewport if inside erased page" { +test "PageList erase resets viewport to active if moves within active" { const testing = std.testing; const alloc = testing.allocator; @@ -1498,7 +1525,50 @@ test "PageList erase resets viewport if inside erased page" { // 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.exact.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.exact.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" { diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index 1b6071608..4aa67e3a3 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -1651,3 +1651,61 @@ test "Screen: clone one line active with extra space" { try testing.expectEqualStrings("1ABC", contents); } } + +test "Screen: clear history with no history" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 3, 3); + defer s.deinit(); + try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); + try testing.expect(s.pages.viewport == .active); + s.eraseRows(.{ .history = .{} }, null); + try testing.expect(s.pages.viewport == .active); + { + // Test our contents rotated + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); + } + { + // Test our contents rotated + const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); + } +} + +test "Screen: clear history" { + 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"); + try testing.expect(s.pages.viewport == .active); + + // Scroll to top + s.scroll(.{ .top = {} }); + { + // Test our contents rotated + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); + } + + s.eraseRows(.{ .history = .{} }, null); + try testing.expect(s.pages.viewport == .active); + { + // Test our contents rotated + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); + } + { + // Test our contents rotated + const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); + } +}