From e55f2daf9075e567b8287ddb0ba049712731ff1d Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 29 Mar 2024 13:08:38 -0400 Subject: [PATCH 1/5] perf(terminal): clear unprotected cells in spans Previous behavior of clearing one at a time hit a page integrity assertion after clearing a wide character but not its tail. This fixes that and should also be - in theory - significantly more performant as well by identifying spans of unprotected cells and clearing them in bulk. --- src/terminal/Screen.zig | 18 ++++++++++++++---- src/terminal/Terminal.zig | 33 ++++++++++----------------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index a94a24c7a..61af34338 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -891,10 +891,20 @@ pub fn clearUnprotectedCells( row: *Row, cells: []Cell, ) void { - for (cells) |*cell| { - if (cell.protected) continue; - const cell_multi: [*]Cell = @ptrCast(cell); - self.clearCells(page, row, cell_multi[0..1]); + var x0: usize = 0; + var x1: usize = 0; + + while (x0 < cells.len) clear: { + while (cells[x0].protected) { + x0 += 1; + if (x0 >= cells.len) break :clear; + } + x1 = x0 + 1; + while (x1 < cells.len and !cells[x1].protected) { + x1 += 1; + } + self.clearCells(page, row, cells[x0..x1]); + x0 = x1; } page.assertIntegrity(); diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 2c17c051a..0c91cb428 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1793,19 +1793,11 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { return; } - // SLOW PATH - // We had a protection mode at some point. We must go through each - // cell and check its protection attribute. - for (0..end) |x| { - const cell_multi: [*]Cell = @ptrCast(cells + x); - const cell: *Cell = @ptrCast(&cell_multi[0]); - if (cell.protected) continue; - self.screen.clearCells( - &self.screen.cursor.page_pin.page.data, - self.screen.cursor.page_row, - cell_multi[0..1], - ); - } + self.screen.clearUnprotectedCells( + &self.screen.cursor.page_pin.page.data, + self.screen.cursor.page_row, + cells[0..end], + ); } /// Erase the line. @@ -1878,16 +1870,11 @@ pub fn eraseLine( return; } - for (start..end) |x| { - const cell_multi: [*]Cell = @ptrCast(cells + x); - const cell: *Cell = @ptrCast(&cell_multi[0]); - if (cell.protected) continue; - self.screen.clearCells( - &self.screen.cursor.page_pin.page.data, - self.screen.cursor.page_row, - cell_multi[0..1], - ); - } + self.screen.clearUnprotectedCells( + &self.screen.cursor.page_pin.page.data, + self.screen.cursor.page_row, + cells[start..end], + ); } /// Erase the display. From 20ab4ec01fef4098364404010d552082a5550acd Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 29 Mar 2024 13:12:52 -0400 Subject: [PATCH 2/5] fix(terminal): correct wrap logic in insert/deleteLines Appropriately handles clearing spacer heads if shifted lines include rightmost column, and centralizes clearing of row wrap state for full width scrolling regions. --- src/terminal/Terminal.zig | 73 +++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 0c91cb428..6dbe2345a 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1359,8 +1359,31 @@ pub fn insertLines(self: *Terminal, count: usize) void { var it = bot.rowIterator(.left_up, top); while (it.next()) |p| { const dst_p = p.down(adjusted_count).?; - const src: *Row = p.rowAndCell().row; - const dst: *Row = dst_p.rowAndCell().row; + const src_rac = p.rowAndCell(); + const dst_rac = dst_p.rowAndCell(); + const src: *Row = src_rac.row; + const dst: *Row = dst_rac.row; + + // If our scrolling region includes the rightmost column then we + // need to turn any spacer heads in to normal empty cells, since + // once we move them they no longer correspond with soft-wrapped + // wide characters. + if (self.scrolling_region.right == self.cols - 1) { + const src_end_cell: *Cell = @ptrCast(@as([*]Cell, @ptrCast(src_rac.cell)) + p.page.data.size.cols - 1); + const dst_end_cell: *Cell = @ptrCast(@as([*]Cell, @ptrCast(dst_rac.cell)) + dst_p.page.data.size.cols - 1); + if (dst_end_cell.wide == .spacer_head) { + dst_end_cell.wide = .narrow; + } + if (src_end_cell.wide == .spacer_head) { + src_end_cell.wide = .narrow; + } + + // If our scrolling region is full width, then we unset wrap. + if (self.scrolling_region.left == 0) { + dst.wrap = false; + src.wrap = false; + } + } // If our page doesn't match, then we need to do a copy from // one page to another. This is the slow path. @@ -1376,9 +1399,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { @panic("TODO"); }; - // Row never is wrapped if we're full width. - if (!left_right) dst.wrap = false; - continue; } @@ -1389,10 +1409,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { dst.* = src.*; src.* = dst_row; - // Row never is wrapped - dst.wrap = false; - src.wrap = false; - // Ensure what we did didn't corrupt the page p.page.data.assertIntegrity(); continue; @@ -1407,9 +1423,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { self.scrolling_region.left, (self.scrolling_region.right - self.scrolling_region.left) + 1, ); - - // Row never is wrapped - dst.wrap = false; } // The operations above can prune our cursor style so we need to @@ -1498,8 +1511,31 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { var it = top.rowIterator(.right_down, bot); while (it.next()) |p| { const src_p = p.down(count).?; - const src: *Row = src_p.rowAndCell().row; - const dst: *Row = p.rowAndCell().row; + const src_rac = src_p.rowAndCell(); + const dst_rac = p.rowAndCell(); + const src: *Row = src_rac.row; + const dst: *Row = dst_rac.row; + + // If our scrolling region includes the rightmost column then we + // need to turn any spacer heads in to normal empty cells, since + // once we move them they no longer correspond with soft-wrapped + // wide characters. + if (self.scrolling_region.right == self.cols - 1) { + const src_end_cell: *Cell = @ptrCast(@as([*]Cell, @ptrCast(src_rac.cell)) + src_p.page.data.size.cols - 1); + const dst_end_cell: *Cell = @ptrCast(@as([*]Cell, @ptrCast(dst_rac.cell)) + p.page.data.size.cols - 1); + if (dst_end_cell.wide == .spacer_head) { + dst_end_cell.wide = .narrow; + } + if (src_end_cell.wide == .spacer_head) { + src_end_cell.wide = .narrow; + } + + // If our scrolling region is full width, then we unset wrap. + if (self.scrolling_region.left == 0) { + dst.wrap = false; + src.wrap = false; + } + } if (src_p.page != p.page) { p.page.data.clonePartialRowFrom( @@ -1513,9 +1549,6 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { @panic("TODO"); }; - // Row never is wrapped if we're full width. - if (!left_right) dst.wrap = false; - continue; } @@ -1526,9 +1559,6 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { dst.* = src.*; src.* = dst_row; - // Row never is wrapped - dst.wrap = false; - // Ensure what we did didn't corrupt the page p.page.data.assertIntegrity(); continue; @@ -1543,9 +1573,6 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { self.scrolling_region.left, (self.scrolling_region.right - self.scrolling_region.left) + 1, ); - - // Row never is wrapped - dst.wrap = false; } // The operations above can prune our cursor style so we need to From 4c9e238c3f32bc3ba49afd484bfcde48f2a84f94 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 29 Mar 2024 13:15:24 -0400 Subject: [PATCH 3/5] fix(termio/exec): avoid overflow in setCursorRow/ColRelative Using a saturating addition here just to avoid overflow, since setCursorPos handles proper clamping to the screen size so we don't need to duplicate that logic. --- src/termio/Exec.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 43708ed2b..27ac5a078 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1993,7 +1993,7 @@ const StreamHandler = struct { pub fn setCursorColRelative(self: *StreamHandler, offset: u16) !void { self.terminal.setCursorPos( self.terminal.screen.cursor.y + 1, - self.terminal.screen.cursor.x + 1 + offset, + self.terminal.screen.cursor.x + 1 +| offset, ); } @@ -2003,7 +2003,7 @@ const StreamHandler = struct { pub fn setCursorRowRelative(self: *StreamHandler, offset: u16) !void { self.terminal.setCursorPos( - self.terminal.screen.cursor.y + 1 + offset, + self.terminal.screen.cursor.y + 1 +| offset, self.terminal.screen.cursor.x + 1, ); } From 925c7e86a274ce5d952845bcc8d788848eab4fa5 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 29 Mar 2024 16:29:27 -0400 Subject: [PATCH 4/5] fix(terminal): insert/deleteLines boundary cond.s Introduced a helper function for correctly handling boundary conditions in insertLines and deleteLines. Also adds a whole host of tests for said conditions in deleteLines, tests not duplicated for insertLines because they both use the same helper function. --- src/terminal/Screen.zig | 19 +++ src/terminal/Terminal.zig | 351 ++++++++++++++++++++++++++++++++++---- 2 files changed, 334 insertions(+), 36 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 61af34338..4467db866 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2147,6 +2147,25 @@ pub fn dumpStringAlloc( return try builder.toOwnedSlice(); } +/// You should use dumpString, this is a restricted version mostly for +/// legacy and convenience reasons for unit tests. +pub fn dumpStringAllocUnwrapped( + self: *const Screen, + alloc: Allocator, + tl: point.Point, +) ![]const u8 { + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + + try self.dumpString(builder.writer(), .{ + .tl = self.pages.getTopLeft(tl), + .br = self.pages.getBottomRight(tl) orelse return error.UnknownPoint, + .unwrap = true, + }); + + return try builder.toOwnedSlice(); +} + /// This is basically a really jank version of Terminal.printString. We /// have to reimplement it here because we want a way to print to the screen /// to test it but don't want all the features of Terminal. diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 6dbe2345a..70bf9cfcc 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1306,6 +1306,63 @@ pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) !void { }); } +/// To be called before shifting a row (as in insertLines and deleteLines) +/// +/// Takes care of boundary conditions such as potentially split wide chars +/// across scrolling region boundaries and orphaned spacer heads at line +/// ends. +fn rowWillBeShifted( + self: *Terminal, + page: *Page, + row: *Row, +) void { + const cells = row.cells.ptr(page.memory.ptr); + + // If our scrolling region includes the rightmost column then we + // need to turn any spacer heads in to normal empty cells, since + // once we move them they no longer correspond with soft-wrapped + // wide characters. + // + // If it contains either of the 2 leftmost columns, then the wide + // characters in the first column which may be associated with a + // spacer head will be either moved or cleared, so we also need + // to turn the spacer heads in to empty cells in that case. + if (self.scrolling_region.right == self.cols - 1 or + self.scrolling_region.left < 2 + ) { + const end_cell: *Cell = &cells[page.size.cols - 1]; + if (end_cell.wide == .spacer_head) { + end_cell.wide = .narrow; + } + } + + // If the leftmost or rightmost cells of our scrolling region + // are parts of wide chars, we need to clear the cells' contents + // since they'd be split by the move. + const left_cell: *Cell = &cells[self.scrolling_region.left]; + const right_cell: *Cell = &cells[self.scrolling_region.right]; + + if (left_cell.wide == .spacer_tail) { + const wide_cell: *Cell = &cells[self.scrolling_region.left - 1]; + if (wide_cell.hasGrapheme()) { + page.clearGrapheme(row, wide_cell); + } + wide_cell.content.codepoint = 0; + wide_cell.wide = .narrow; + left_cell.wide = .narrow; + } + + if (right_cell.wide == .wide) { + const tail_cell: *Cell = &cells[self.scrolling_region.right + 1]; + if (right_cell.hasGrapheme()) { + page.clearGrapheme(row, right_cell); + } + right_cell.content.codepoint = 0; + right_cell.wide = .narrow; + tail_cell.wide = .narrow; + } +} + /// Insert amount lines at the current cursor row. The contents of the line /// at the current cursor row and below (to the bottom-most line in the /// scrolling region) are shifted down by amount lines. The contents of the @@ -1364,25 +1421,15 @@ pub fn insertLines(self: *Terminal, count: usize) void { const src: *Row = src_rac.row; const dst: *Row = dst_rac.row; - // If our scrolling region includes the rightmost column then we - // need to turn any spacer heads in to normal empty cells, since - // once we move them they no longer correspond with soft-wrapped - // wide characters. - if (self.scrolling_region.right == self.cols - 1) { - const src_end_cell: *Cell = @ptrCast(@as([*]Cell, @ptrCast(src_rac.cell)) + p.page.data.size.cols - 1); - const dst_end_cell: *Cell = @ptrCast(@as([*]Cell, @ptrCast(dst_rac.cell)) + dst_p.page.data.size.cols - 1); - if (dst_end_cell.wide == .spacer_head) { - dst_end_cell.wide = .narrow; - } - if (src_end_cell.wide == .spacer_head) { - src_end_cell.wide = .narrow; - } + self.rowWillBeShifted(&p.page.data, src); + self.rowWillBeShifted(&dst_p.page.data, dst); - // If our scrolling region is full width, then we unset wrap. - if (self.scrolling_region.left == 0) { - dst.wrap = false; - src.wrap = false; - } + // If our scrolling region is full width, then we unset wrap. + if (!left_right) { + dst.wrap = false; + src.wrap = false; + dst.wrap_continuation = false; + src.wrap_continuation = false; } // If our page doesn't match, then we need to do a copy from @@ -1516,25 +1563,15 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { const src: *Row = src_rac.row; const dst: *Row = dst_rac.row; - // If our scrolling region includes the rightmost column then we - // need to turn any spacer heads in to normal empty cells, since - // once we move them they no longer correspond with soft-wrapped - // wide characters. - if (self.scrolling_region.right == self.cols - 1) { - const src_end_cell: *Cell = @ptrCast(@as([*]Cell, @ptrCast(src_rac.cell)) + src_p.page.data.size.cols - 1); - const dst_end_cell: *Cell = @ptrCast(@as([*]Cell, @ptrCast(dst_rac.cell)) + p.page.data.size.cols - 1); - if (dst_end_cell.wide == .spacer_head) { - dst_end_cell.wide = .narrow; - } - if (src_end_cell.wide == .spacer_head) { - src_end_cell.wide = .narrow; - } + self.rowWillBeShifted(&src_p.page.data, src); + self.rowWillBeShifted(&p.page.data, dst); - // If our scrolling region is full width, then we unset wrap. - if (self.scrolling_region.left == 0) { - dst.wrap = false; - src.wrap = false; - } + // If our scrolling region is full width, then we unset wrap. + if (!left_right) { + dst.wrap = false; + src.wrap = false; + dst.wrap_continuation = false; + src.wrap_continuation = false; } if (src_p.page != p.page) { @@ -2399,6 +2436,11 @@ pub fn plainString(self: *Terminal, alloc: Allocator) ![]const u8 { return try self.screen.dumpStringAlloc(alloc, .{ .viewport = .{} }); } +/// Same as plainString, but respects row wrap state when building the string. +pub fn plainStringUnwrapped(self: *Terminal, alloc: Allocator) ![]const u8 { + return try self.screen.dumpStringAllocUnwrapped(alloc, .{ .viewport = .{} }); +} + /// Full reset pub fn fullReset(self: *Terminal) void { // Switch back to primary screen and clear it. We do not restore cursor @@ -6133,6 +6175,243 @@ test "Terminal: deleteLines left/right scroll region high count" { } } +test "Terminal: deleteLines wide character spacer head" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + // Initial value + // +-----+ + // |AAAAA| < Wrapped + // |BBBB*| < Wrapped (continued) + // |WWCCC| < Non-wrapped (continued) + // +-----+ + // where * represents a spacer head cell + // and WW is the wide character. + try t.printString("AAAAABBBB\u{1F600}CCC"); + + // Delete the top line + // +-----+ + // |BBBB | < Non-wrapped + // |WWCCC| < Non-wrapped + // | | < Non-wrapped + // +-----+ + // This should convert the spacer head to + // a regular empty cell, and un-set wrap. + t.setCursorPos(1, 1); + t.deleteLines(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); + defer testing.allocator.free(unwrapped_str); + try testing.expectEqualStrings("BBBB \n\u{1F600}CCC", str); + try testing.expectEqualStrings("BBBB \n\u{1F600}CCC", unwrapped_str); + } +} + +test "Terminal: deleteLines wide character spacer head left scroll margin" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + // Initial value + // +-----+ + // |AAAAA| < Wrapped + // |BBBB*| < Wrapped (continued) + // |WWCCC| < Non-wrapped (continued) + // +-----+ + // where * represents a spacer head cell + // and WW is the wide character. + try t.printString("AAAAABBBB\u{1F600}CCC"); + + t.scrolling_region.left = 2; + + // Delete the top line + // ### <- scrolling region + // +-----+ + // |AABB | < Wrapped + // |BBCCC| < Wrapped (continued) + // |WW | < Non-wrapped (continued) + // +-----+ + // This should convert the spacer head to + // a regular empty cell, but due to the + // left scrolling margin, wrap state should + // remain. + t.setCursorPos(1, 3); + t.deleteLines(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); + defer testing.allocator.free(unwrapped_str); + try testing.expectEqualStrings("AABB \nBBCCC\n\u{1F600}", str); + try testing.expectEqualStrings("AABB BBCCC\u{1F600}", unwrapped_str); + } +} + +test "Terminal: deleteLines wide character spacer head right scroll margin" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + // Initial value + // +-----+ + // |AAAAA| < Wrapped + // |BBBB*| < Wrapped (continued) + // |WWCCC| < Non-wrapped (continued) + // +-----+ + // where * represents a spacer head cell + // and WW is the wide character. + try t.printString("AAAAABBBB\u{1F600}CCC"); + + t.scrolling_region.right = 3; + + // Delete the top line + // #### <- scrolling region + // +-----+ + // |BBBBA| < Wrapped + // |WWCC | < Wrapped (continued) + // | C| < Non-wrapped (continued) + // +-----+ + // This should convert the spacer head to + // a regular empty cell, but due to the + // right scrolling margin, wrap state should + // remain. + t.setCursorPos(1, 1); + t.deleteLines(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); + defer testing.allocator.free(unwrapped_str); + try testing.expectEqualStrings("BBBBA\n\u{1F600}CC \n C", str); + try testing.expectEqualStrings("BBBBA\u{1F600}CC C", unwrapped_str); + } +} + +test "Terminal: deleteLines wide character spacer head left and right scroll margin" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + // Initial value + // +-----+ + // |AAAAA| < Wrapped + // |BBBB*| < Wrapped (continued) + // |WWCCC| < Non-wrapped (continued) + // +-----+ + // where * represents a spacer head cell + // and WW is the wide character. + try t.printString("AAAAABBBB\u{1F600}CCC"); + + t.scrolling_region.right = 3; + t.scrolling_region.left = 2; + + // Delete the top line + // ## <- scrolling region + // +-----+ + // |AABBA| < Wrapped + // |BBCC*| < Wrapped (continued) + // |WW C| < Non-wrapped (continued) + // +-----+ + // Because there is both a left scrolling + // margin > 1 and a right scrolling margin + // the spacer head should remain, and the + // wrap state should be untouched. + t.setCursorPos(1, 3); + t.deleteLines(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); + defer testing.allocator.free(unwrapped_str); + try testing.expectEqualStrings("AABBA\nBBCC\n\u{1F600} C", str); + try testing.expectEqualStrings("AABBABBCC\u{1F600} C", unwrapped_str); + } +} + +test "Terminal: deleteLines wide character spacer head left (< 2) and right scroll margin" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 5, .rows = 3 }); + defer t.deinit(alloc); + + // Initial value + // +-----+ + // |AAAAA| < Wrapped + // |BBBB*| < Wrapped (continued) + // |WWCCC| < Non-wrapped (continued) + // +-----+ + // where * represents a spacer head cell + // and WW is the wide character. + try t.printString("AAAAABBBB\u{1F600}CCC"); + + t.scrolling_region.right = 3; + t.scrolling_region.left = 1; + + // Delete the top line + // ### <- scrolling region + // +-----+ + // |ABBBA| < Wrapped + // |B CC | < Wrapped (continued) + // | C| < Non-wrapped (continued) + // +-----+ + // Because the left margin is 1, the wide + // char is split, and therefore removed, + // along with the spacer head - however, + // wrap state should be untouched. + t.setCursorPos(1, 2); + t.deleteLines(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); + defer testing.allocator.free(unwrapped_str); + try testing.expectEqualStrings("ABBBA\nB CC \n C", str); + try testing.expectEqualStrings("ABBBAB CC C", unwrapped_str); + } +} + +test "Terminal: deleteLines wide characters split by left/right scroll region boundaries" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 5, .rows = 2 }); + defer t.deinit(alloc); + + // Initial value + // +-----+ + // |AAAAA| + // |WWBWW| + // +-----+ + // where WW represents a wide character + try t.printString("AAAAA\n\u{1F600}B\u{1F600}"); + + t.scrolling_region.right = 3; + t.scrolling_region.left = 1; + + // Delete the top line + // ### <- scrolling region + // +-----+ + // |A B A| + // | | + // +-----+ + // The two wide chars, because they're + // split by the edge of the scrolling + // region, get removed. + t.setCursorPos(1, 2); + t.deleteLines(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("A B A\n ", str); + } +} + test "Terminal: deleteLines zero" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); From 5b509f929595f80f45a65123053b387d43ce7756 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 29 Mar 2024 16:47:53 -0400 Subject: [PATCH 5/5] test(terminal/Screen): clearRows with protected cells --- src/terminal/Screen.zig | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 4467db866..96c84ce9b 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2219,6 +2219,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { .content_tag = .codepoint, .content = .{ .codepoint = c }, .style_id = self.cursor.style_id, + .protected = self.cursor.protected, }; // If we have a ref-counted style, increase. @@ -2235,6 +2236,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { .content_tag = .codepoint, .content = .{ .codepoint = 0 }, .wide = .spacer_head, + .protected = self.cursor.protected, }; self.cursor.page_row.wrap = true; @@ -2249,6 +2251,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { .content = .{ .codepoint = c }, .style_id = self.cursor.style_id, .wide = .wide, + .protected = self.cursor.protected, }; // Write our tail @@ -2257,6 +2260,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { .content_tag = .codepoint, .content = .{ .codepoint = 0 }, .wide = .spacer_tail, + .protected = self.cursor.protected, }; }, @@ -2537,6 +2541,34 @@ test "Screen clearRows active styled line" { try testing.expectEqualStrings("", str); } +test "Screen clearRows protected" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + + try s.testWriteString("UNPROTECTED"); + s.cursor.protected = true; + try s.testWriteString("PROTECTED"); + s.cursor.protected = false; + try s.testWriteString("UNPROTECTED"); + try s.testWriteString("\n"); + s.cursor.protected = true; + try s.testWriteString("PROTECTED"); + s.cursor.protected = false; + try s.testWriteString("UNPROTECTED"); + s.cursor.protected = true; + try s.testWriteString("PROTECTED"); + s.cursor.protected = false; + + s.clearRows(.{ .active = .{} }, null, true); + + const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); + defer alloc.free(str); + try testing.expectEqualStrings(" PROTECTED\nPROTECTED PROTECTED", str); +} + test "Screen eraseRows history" { const testing = std.testing; const alloc = testing.allocator;