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.
This commit is contained in:
Qwerasd
2024-03-29 16:29:27 -04:00
parent 4c9e238c3f
commit 925c7e86a2
2 changed files with 334 additions and 36 deletions

View File

@ -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.

View File

@ -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 });