From be3a539152d0d4cc80fda2f93ff412d05db26ac1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Aug 2022 15:52:53 -0700 Subject: [PATCH 01/20] start working on new resize with reflow, can grow rows --- src/terminal/Screen.zig | 176 ++++++++++++++++++++++++++++++++------ src/terminal/Terminal.zig | 2 +- 2 files changed, 152 insertions(+), 26 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 856d36a3f..7d9fd76be 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -431,6 +431,51 @@ pub fn copyRow(self: *Screen, dst: usize, src: usize) void { 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 resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { + _ = cols; + + // We always grow first so we don't lose any data. + var storage = self.storage; + if (rows > self.rows) { + storage = try alloc.alloc( + Cell, + (rows + self.max_scrollback) * 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; + + // 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 = {} }); + } +} + /// 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 @@ -608,13 +653,15 @@ fn selectionSlices(self: Screen, sel: Selection) struct { }; } -/// Turns the screen into a string. -pub fn testString(self: Screen, alloc: Allocator) ![]const u8 { +/// 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); var i: usize = 0; var y: usize = 0; - var rows = self.rowIterator(.viewport); + var rows = self.rowIterator(tag); while (rows.next()) |row| { defer y += 1; @@ -642,16 +689,23 @@ pub fn testString(self: Screen, alloc: Allocator) ![]const u8 { fn testWriteString(self: *Screen, text: []const u8) void { var y: usize = 0; var x: usize = 0; - var row = self.getRow(.{ .active = y }); for (text) |c| { // Explicit newline forces a new row if (c == '\n') { y += 1; x = 0; - row = self.getRow(.{ .active = y }); 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 = 1; @@ -676,7 +730,7 @@ test "Screen" { const str = "1ABCD\n2EFGH\n3IJKL"; s.testWriteString(str); { - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .screen); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } @@ -699,7 +753,7 @@ test "Screen" { std.mem.set(Cell, reg[0], .{ .char = 'A' }); std.mem.set(Cell, reg[1], .{ .char = 'A' }); { - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .screen); defer alloc.free(contents); try testing.expectEqualStrings("AAAAA\nAAAAA\nAAAAA", contents); } @@ -726,7 +780,7 @@ test "Screen: scrolling" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); } @@ -736,7 +790,7 @@ test "Screen: scrolling" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); } @@ -775,7 +829,7 @@ test "Screen: scroll down from 0" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); } @@ -797,7 +851,7 @@ test "Screen: scrollback" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); } @@ -808,7 +862,7 @@ test "Screen: scrollback" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); } @@ -819,7 +873,7 @@ test "Screen: scrollback" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); } @@ -829,7 +883,7 @@ test "Screen: scrollback" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); } @@ -839,7 +893,7 @@ test "Screen: scrollback" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); } @@ -849,7 +903,7 @@ test "Screen: scrollback" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); } @@ -859,7 +913,7 @@ test "Screen: scrollback" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); } @@ -869,7 +923,7 @@ test "Screen: scrollback" { std.mem.set(Cell, reg[0], .{ .char = 0 }); std.mem.set(Cell, reg[1], .{ .char = 0 }); { - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD", contents); } @@ -879,7 +933,7 @@ test "Screen: scrollback" { { // Test our contents rotated - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("", contents); } @@ -896,7 +950,7 @@ test "Screen: scrollback empty" { { // Test our contents - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); } @@ -915,7 +969,7 @@ test "Screen: row copy" { s.copyRow(2, 0); // Test our contents - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL\n2EFGH", contents); } @@ -931,7 +985,7 @@ test "Screen: resize more rows" { try s.resize(alloc, 10, 5); { - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } @@ -948,7 +1002,7 @@ test "Screen: resize less rows" { try s.resize(alloc, 2, 5); { - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); } @@ -965,7 +1019,7 @@ test "Screen: resize more cols" { try s.resize(alloc, 3, 10); { - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } @@ -982,7 +1036,7 @@ test "Screen: resize less cols" { try s.resize(alloc, 3, 4); { - var contents = try s.testString(alloc); + var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); const expected = "1ABC\n2EFG\n3IJK"; try testing.expectEqualStrings(expected, contents); @@ -1055,3 +1109,75 @@ test "Screen: selectionString wrap around" { try testing.expectEqualStrings(expected, contents); } } + +// ---------------------------------------------------------------------------- +// NEW RESIZE TESTS + +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); + try s.resize2(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 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); + try s.resize2(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 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); + } + + // Resize + try s.resize2(alloc, 10, 5); + + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } +} diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 775ca0452..6c37a80d9 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -252,7 +252,7 @@ pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) ! /// /// The caller must free the string. pub fn plainString(self: Terminal, alloc: Allocator) ![]const u8 { - return try self.screen.testString(alloc); + return try self.screen.testString(alloc, .viewport); } /// Save cursor position and further state. From dc351582bcc2b3633c781c9c62232cf0973376d3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Aug 2022 16:23:23 -0700 Subject: [PATCH 02/20] resize increasing column width without reflow --- src/terminal/Screen.zig | 96 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 7d9fd76be..ffbe6af2f 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -444,12 +444,12 @@ pub fn copyRow(self: *Screen, dst: usize, src: usize) void { pub fn resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { _ = cols; - // We always grow first so we don't lose any data. - var storage = self.storage; + // 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) { - storage = try alloc.alloc( + var storage = try alloc.alloc( Cell, - (rows + self.max_scrollback) * cols, + (rows + self.max_scrollback) * self.cols, ); // Copy our screen into the new storage area. Since we're growing @@ -474,6 +474,44 @@ pub fn resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void self.bottom = @maximum(rows, self.bottom); self.scroll(.{ .bottom = {} }); } + + // 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 }); + + // 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); + old.deinit(alloc); + } + self.storage = storage; + self.cols = cols; + + // 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); + + // If no reflow, just keep going + if (row[row.len - 1].attrs.wrap == 0) { + y += 1; + continue; + } + + @panic("REFLOW"); + } + } } /// Resize the screen. The rows or cols can be bigger or smaller. This @@ -1144,6 +1182,7 @@ test "Screen: resize more rows with empty scrollback" { const str = "1ABCD\n2EFGH\n3IJKL"; s.testWriteString(str); try s.resize2(alloc, 10, 5); + try testing.expectEqual(@as(usize, 20), s.totalRows()); { var contents = try s.testString(alloc, .viewport); @@ -1174,6 +1213,7 @@ test "Screen: resize more rows with populated scrollback" { // Resize try s.resize2(alloc, 10, 5); + try testing.expectEqual(@as(usize, 15), s.totalRows()); { var contents = try s.testString(alloc, .viewport); @@ -1181,3 +1221,51 @@ test "Screen: resize more rows with populated scrollback" { 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); + try s.resize2(alloc, 3, 10); + + { + 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); +// } +// +// // Resize and verify we undid the soft wrap because we have space now +// try s.resize2(alloc, 3, 10); +// { +// var contents = try s.testString(alloc, .viewport); +// defer alloc.free(contents); +// try testing.expectEqualStrings(str, contents); +// } +// } From 2cf4a265e53e2282ebaf3949e750c38617942fec Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 09:36:04 -0700 Subject: [PATCH 03/20] reflow when cols grow --- src/terminal/Screen.zig | 145 +++++++++++++++++++++++++++++++++------- 1 file changed, 119 insertions(+), 26 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index ffbe6af2f..8699bbf5b 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -509,7 +509,46 @@ pub fn resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void continue; } - @panic("REFLOW"); + // 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 = 0; + new_row = new_row[row.len..]; + wrapping: while (iter.next()) |wrapped_row| { + var wrapped_rem = wrapped_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 this row isn't also wrapped, we're done! + if (wrapped_rem[wrapped_rem.len - 1].attrs.wrap == 0) { + y += 1; + break :wrapping; + } + + // Wrapped again! + new_row[wrapped_rem.len - 1].attrs.wrap = 0; + new_row = new_row[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 = 1; + + // We still need to copy the remainder + wrapped_rem = wrapped_rem[new_row.len..]; + + // Move to a new line in our new screen + y += 1; + new_row = self.getRow(.{ .screen = y }); + } + } } } } @@ -1244,28 +1283,82 @@ test "Screen: resize more cols no reflow" { } } -// 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); -// } -// -// // Resize and verify we undid the soft wrap because we have space now -// try s.resize2(alloc, 3, 10); -// { -// var contents = try s.testString(alloc, .viewport); -// 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); + } + + // Resize and verify we undid the soft wrap because we have space now + try s.resize2(alloc, 3, 10); + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } +} + +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); + + // 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.resize2(alloc, 3, 7); + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + const expected = "1ABCD2E\nFGH\n3IJKL"; + try testing.expectEqualStrings(expected, contents); + } +} + +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); + + // 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.resize2(alloc, 3, 15); + { + var contents = try s.testString(alloc, .viewport); + defer alloc.free(contents); + const expected = "1ABCD2EFGH3IJKL"; + try testing.expectEqualStrings(expected, contents); + } +} From 4ef73efeac628d25e1ac57733555431bc21986ec Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 10:28:11 -0700 Subject: [PATCH 04/20] resize less rows, but currently not 100% working (see commented tests) --- src/terminal/Screen.zig | 118 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 2 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 8699bbf5b..c39c8c438 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -442,8 +442,6 @@ pub fn copyRow(self: *Screen, dst: usize, src: usize) void { /// This will trim data if the size is getting smaller. This will reflow the /// soft wrapped text. pub fn resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { - _ = 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) { @@ -551,6 +549,48 @@ pub fn resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void } } } + + // 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); + const bot_len = @minimum(reg[1].len, storage.len); + const top_len = @minimum(reg[0].len, storage.len - bot_len); + std.mem.copy(Cell, storage, reg[0][reg[0].len - top_len ..]); + std.mem.copy(Cell, storage[top_len..], reg[1][reg[1].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; + + //log.warn("bot={} top={} copied={}", .{ bot_len, top_len, copied_rows }); + + // Modify our storage + alloc.free(self.storage); + self.storage = storage; + + // 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); + //self.bottom = @minimum(self.bottom, copied_rows); + log.warn("BOTTOM={}", .{self.bottom}); + self.scroll(.{ .bottom = {} }); + } } /// Resize the screen. The rows or cols can be bigger or smaller. This @@ -1362,3 +1402,77 @@ test "Screen: resize more cols with reflow that unwraps multiple times" { 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); + try s.resize2(alloc, 1, 5); + + { + 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.resize2(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 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); +// } +// +// // Resize +// try s.resize2(alloc, 10, 5); +// try testing.expectEqual(@as(usize, 15), s.totalRows()); +// +// { +// var contents = try s.testString(alloc, .viewport); +// defer alloc.free(contents); +// try testing.expectEqualStrings(str, contents); +// } +// } +// From 57f698634363e473424c5438ac752118ddc2953d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 10:32:42 -0700 Subject: [PATCH 05/20] rowIterator(.screen) now ignores unused lines, fixes shrinking rows --- src/terminal/Screen.zig | 112 +++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index c39c8c438..33d029ede 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -126,7 +126,10 @@ pub const RowIndexTag = enum { /// The max value for the given tag. pub fn max(self: RowIndexTag, screen: *const Screen) usize { return switch (self) { - .screen => screen.totalRows(), + // 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, } - 1; @@ -265,7 +268,7 @@ pub fn getCell(self: Screen, row: usize, col: usize) *Cell { fn rowIndex(self: Screen, idx: RowIndex) usize { const y = switch (idx) { .screen => |y| y: { - assert(y < self.totalRows()); + assert(y < self.bottom); break :y y; }, @@ -573,8 +576,6 @@ pub fn resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void assert(@mod(top_len + bot_len, self.cols) == 0); const copied_rows = (top_len + bot_len) / self.cols; - //log.warn("bot={} top={} copied={}", .{ bot_len, top_len, copied_rows }); - // Modify our storage alloc.free(self.storage); self.storage = storage; @@ -588,7 +589,8 @@ pub fn resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void self.top = 0; self.bottom = @maximum(rows, copied_rows); //self.bottom = @minimum(self.bottom, copied_rows); - log.warn("BOTTOM={}", .{self.bottom}); + //log.warn("bot={} top={} copied={}", .{ bot_len, top_len, copied_rows }); + //log.warn("BOTTOM={}", .{self.bottom}); self.scroll(.{ .bottom = {} }); } } @@ -1427,52 +1429,56 @@ test "Screen: resize less rows no scrollback" { } } -// 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.resize2(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 empty scrollback" { + const testing = std.testing; + const alloc = testing.allocator; -// 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); -// } -// -// // Resize -// try s.resize2(alloc, 10, 5); -// try testing.expectEqual(@as(usize, 15), s.totalRows()); -// -// { -// var contents = try s.testString(alloc, .viewport); -// defer alloc.free(contents); -// try testing.expectEqualStrings(str, contents); -// } -// } -// + var s = try init(alloc, 3, 5, 10); + defer s.deinit(alloc); + const str = "1ABCD\n2EFGH\n3IJKL"; + s.testWriteString(str); + try s.resize2(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.resize2(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); + } +} From 8a5dadd995377ca643aa53f6c7f71e0d6314d3aa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 10:38:24 -0700 Subject: [PATCH 06/20] rowIndex uses tag max so we have max in just one place --- src/terminal/Screen.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 33d029ede..3c129f71b 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -265,20 +265,20 @@ pub fn getCell(self: Screen, row: usize, col: usize) *Cell { /// 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: Screen, idx: RowIndex) usize { +fn rowIndex(self: *const Screen, idx: RowIndex) usize { const y = switch (idx) { .screen => |y| y: { - assert(y < self.bottom); + assert(y <= RowIndexTag.screen.max(self)); break :y y; }, .viewport => |y| y: { - assert(y < self.rows); + assert(y <= RowIndexTag.viewport.max(self)); break :y y + self.visible_offset; }, .active => |y| y: { - assert(y < self.rows); + assert(y <= RowIndexTag.active.max(self)); break :y self.bottomOffset() + y; }, }; From f97b739317a856426311c4545f46ca3497da911d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 13:04:27 -0700 Subject: [PATCH 07/20] reflow when columns get smaller --- src/terminal/Screen.zig | 168 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 3c129f71b..cd626a1cc 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -593,6 +593,80 @@ pub fn resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void //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 }); + + // 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); + old.deinit(alloc); + } + self.storage = storage; + self.cols = cols; + + // 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| { + // Soft wrap if we have to + if (x == self.cols) { + var last_cell = self.getCell(y, x - 1); + last_cell.attrs.wrap = 1; + 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 -= 1; + } + + // Copy the old cell, unset the old wrap state + var new_cell = self.getCell(y, x); + new_cell.* = cell; + new_cell.attrs.wrap = 0; + + // Next + x += 1; + } + + // 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 == 0) + { + y += 1; + x = 0; + } + } + } } /// Resize the screen. The rows or cols can be bigger or smaller. This @@ -1482,3 +1556,97 @@ 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(alloc); + const str = "1AB\n2EF\n3IJ"; + s.testWriteString(str); + try s.resize2(alloc, 3, 3); + + { + 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); + try s.resize2(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); + } +} + +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.resize2(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.resize2(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); + } +} From 9ddb19e9b0ca9afe796316983cd57929eb606c29 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 13:06:01 -0700 Subject: [PATCH 08/20] replace old resize with new reflow resize --- src/terminal/Screen.zig | 164 ++++------------------------------------ 1 file changed, 15 insertions(+), 149 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index cd626a1cc..bf03f1a45 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -444,7 +444,7 @@ pub fn copyRow(self: *Screen, dst: usize, src: usize) void { /// /// This will trim data if the size is getting smaller. This will reflow the /// soft wrapped text. -pub fn resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { +pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // 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) { @@ -669,68 +669,6 @@ pub fn resize2(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void } } -/// 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 { - // We do this in a pretty inefficient way because this implementation - // is easier and resizing is relatively rare. I welcome anyone to improve - // on this! Our naive approach is to just iterate over the entire screen - // (including scrollback) and reflow the entire thing by rewriting it. - // TODO: above not implemented yet - - // 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); - 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); - - // TODO: reflow due to soft wrap - - // 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 }); - } - - // Free the old data - alloc.free(old.storage); -} - /// 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 { @@ -1167,75 +1105,6 @@ test "Screen: row copy" { try testing.expectEqualStrings("2EFGH\n3IJKL\n2EFGH", contents); } -test "Screen: resize 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.resize(alloc, 10, 5); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize 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.resize(alloc, 2, 5); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings("2EFGH\n3IJKL", contents); - } -} - -test "Screen: resize 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.resize(alloc, 3, 10); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); - } -} - -test "Screen: resize 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.resize(alloc, 3, 4); - - { - var contents = try s.testString(alloc, .viewport); - defer alloc.free(contents); - const expected = "1ABC\n2EFG\n3IJK"; - try testing.expectEqualStrings(expected, contents); - } -} - test "Screen: selectionString" { const testing = std.testing; const alloc = testing.allocator; @@ -1303,9 +1172,6 @@ test "Screen: selectionString wrap around" { } } -// ---------------------------------------------------------------------------- -// NEW RESIZE TESTS - test "Screen: resize more rows no scrollback" { const testing = std.testing; const alloc = testing.allocator; @@ -1314,7 +1180,7 @@ test "Screen: resize more rows no scrollback" { defer s.deinit(alloc); const str = "1ABCD\n2EFGH\n3IJKL"; s.testWriteString(str); - try s.resize2(alloc, 10, 5); + try s.resize(alloc, 10, 5); { var contents = try s.testString(alloc, .viewport); @@ -1336,7 +1202,7 @@ test "Screen: resize more rows with empty scrollback" { defer s.deinit(alloc); const str = "1ABCD\n2EFGH\n3IJKL"; s.testWriteString(str); - try s.resize2(alloc, 10, 5); + try s.resize(alloc, 10, 5); try testing.expectEqual(@as(usize, 20), s.totalRows()); { @@ -1367,7 +1233,7 @@ test "Screen: resize more rows with populated scrollback" { } // Resize - try s.resize2(alloc, 10, 5); + try s.resize(alloc, 10, 5); try testing.expectEqual(@as(usize, 15), s.totalRows()); { @@ -1385,7 +1251,7 @@ test "Screen: resize more cols no reflow" { defer s.deinit(alloc); const str = "1ABCD\n2EFGH\n3IJKL"; s.testWriteString(str); - try s.resize2(alloc, 3, 10); + try s.resize(alloc, 3, 10); { var contents = try s.testString(alloc, .viewport); @@ -1417,7 +1283,7 @@ test "Screen: resize more cols with reflow that fits full width" { } // Resize and verify we undid the soft wrap because we have space now - try s.resize2(alloc, 3, 10); + try s.resize(alloc, 3, 10); { var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); @@ -1443,7 +1309,7 @@ test "Screen: resize more cols with reflow that forces more wrapping" { } // Resize and verify we undid the soft wrap because we have space now - try s.resize2(alloc, 3, 7); + try s.resize(alloc, 3, 7); { var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); @@ -1470,7 +1336,7 @@ test "Screen: resize more cols with reflow that unwraps multiple times" { } // Resize and verify we undid the soft wrap because we have space now - try s.resize2(alloc, 3, 15); + try s.resize(alloc, 3, 15); { var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); @@ -1487,7 +1353,7 @@ test "Screen: resize less rows no scrollback" { defer s.deinit(alloc); const str = "1ABCD\n2EFGH\n3IJKL"; s.testWriteString(str); - try s.resize2(alloc, 1, 5); + try s.resize(alloc, 1, 5); { var contents = try s.testString(alloc, .viewport); @@ -1511,7 +1377,7 @@ test "Screen: resize less rows with empty scrollback" { defer s.deinit(alloc); const str = "1ABCD\n2EFGH\n3IJKL"; s.testWriteString(str); - try s.resize2(alloc, 1, 5); + try s.resize(alloc, 1, 5); { var contents = try s.testString(alloc, .screen); @@ -1542,7 +1408,7 @@ test "Screen: resize less rows with populated scrollback" { } // Resize - try s.resize2(alloc, 1, 5); + try s.resize(alloc, 1, 5); { var contents = try s.testString(alloc, .screen); @@ -1565,7 +1431,7 @@ test "Screen: resize less cols no reflow" { defer s.deinit(alloc); const str = "1AB\n2EF\n3IJ"; s.testWriteString(str); - try s.resize2(alloc, 3, 3); + try s.resize(alloc, 3, 3); { var contents = try s.testString(alloc, .viewport); @@ -1587,7 +1453,7 @@ test "Screen: resize less cols with reflow but row space" { defer s.deinit(alloc); const str = "1ABCD"; s.testWriteString(str); - try s.resize2(alloc, 3, 3); + try s.resize(alloc, 3, 3); { var contents = try s.testString(alloc, .viewport); @@ -1611,7 +1477,7 @@ test "Screen: resize less cols with reflow with trimmed rows" { defer s.deinit(alloc); const str = "3IJKL\n4ABCD\n5EFGH"; s.testWriteString(str); - try s.resize2(alloc, 3, 3); + try s.resize(alloc, 3, 3); { var contents = try s.testString(alloc, .viewport); @@ -1635,7 +1501,7 @@ test "Screen: resize less cols with reflow with trimmed rows and scrollback" { defer s.deinit(alloc); const str = "3IJKL\n4ABCD\n5EFGH"; s.testWriteString(str); - try s.resize2(alloc, 3, 3); + try s.resize(alloc, 3, 3); { var contents = try s.testString(alloc, .viewport); From a37bf60bf67868b74f8829c2c07a2a8edde207c4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 13:06:32 -0700 Subject: [PATCH 09/20] update TODO --- TODO.md | 1 - 1 file changed, 1 deletion(-) diff --git a/TODO.md b/TODO.md index ff3872dbe..7a284dfc6 100644 --- a/TODO.md +++ b/TODO.md @@ -13,7 +13,6 @@ Performance: Correctness: * `exit` in the shell should close the window -* scrollback: reflow on resize * test wrap against wraptest: https://github.com/mattiase/wraptest - automate this in some way From bfbeceeefa998e5ecbe48bcadd4cec39b6b7394f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 13:47:49 -0700 Subject: [PATCH 10/20] when cols grow, move cursor if it unwraps the line it is on --- src/terminal/Screen.zig | 73 ++++++++++++++++++++++++++++++++++++++--- src/terminal/point.zig | 18 ++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index bf03f1a45..2fa93deab 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -29,7 +29,6 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const color = @import("color.zig"); const point = @import("point.zig"); -const Point = point.Point; const Selection = @import("Selection.zig"); const log = std.log.scoped(.screen); @@ -485,13 +484,20 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { ); 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); - old.deinit(alloc); + alloc.free(old.storage); } self.storage = storage; self.cols = cols; @@ -514,9 +520,15 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // The goal is to keep the messiness of reflow down here and // only reloop when we're back to clean non-wrapped lines. + // Whether we need to move the cursor or not + var new_cursor: ?point.ScreenPoint = null; + // Mark the last element as not wrapped new_row[row.len - 1].attrs.wrap = 0; - new_row = new_row[row.len..]; + + // 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| { var wrapped_rem = wrapped_row; while (wrapped_rem.len > 0) { @@ -525,6 +537,13 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // 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) { + assert(new_cursor == null); // should only happen once + 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 == 0) { y += 1; @@ -534,6 +553,7 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // Wrapped again! new_row[wrapped_rem.len - 1].attrs.wrap = 0; new_row = new_row[wrapped_rem.len..]; + x += wrapped_rem.len; break; } @@ -545,11 +565,30 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // 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) + { + if (true) @panic("IN FLOW"); // TODO: to test + 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; + } } } @@ -611,7 +650,7 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { var old = self.*; defer { assert(old.storage.ptr != self.storage.ptr); - old.deinit(alloc); + alloc.free(old.storage); } self.storage = storage; self.cols = cols; @@ -647,6 +686,7 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { if (y >= self.rows) { self.scroll(.{ .delta = 1 }); y -= 1; + x = 0; } // Copy the old cell, unset the old wrap state @@ -1180,8 +1220,12 @@ test "Screen: resize more rows no scrollback" { 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); @@ -1202,9 +1246,13 @@ test "Screen: resize more rows with empty scrollback" { 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); @@ -1233,9 +1281,13 @@ test "Screen: resize more rows with populated scrollback" { } // Resize + const cursor = s.cursor; try s.resize(alloc, 10, 5); try testing.expectEqual(@as(usize, 15), s.totalRows()); + // Cursor should not move + try testing.expectEqual(cursor, s.cursor); + { var contents = try s.testString(alloc, .viewport); defer alloc.free(contents); @@ -1251,8 +1303,12 @@ test "Screen: resize more cols no reflow" { 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); @@ -1282,6 +1338,11 @@ test "Screen: resize more cols with reflow that fits full width" { 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); { @@ -1289,6 +1350,10 @@ test "Screen: resize more cols with reflow that fits full width" { 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 forces more wrapping" { diff --git a/src/terminal/point.zig b/src/terminal/point.zig index 52ca6ec42..85b093bf9 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -76,6 +76,24 @@ pub const ScreenPoint = struct { (self.y == other.y and self.x < other.x); } + /// Converts this to a viewport point. If the point is above the + /// viewport this will move the point to (0, 0) and if it is below + /// the viewport it'll move it to (cols - 1, rows - 1). + pub fn toViewport(self: ScreenPoint, screen: *const Screen) Viewport { + // TODO: test + + // Before viewport + if (self.y < screen.visible_offset) return .{ .x = 0, .y = 0 }; + + // After viewport + if (self.y > screen.visible_offset + screen.rows) return .{ + .x = screen.cols - 1, + .y = screen.rows - 1, + }; + + return .{ .x = self.x, .y = self.y - screen.visible_offset }; + } + test "before" { const testing = std.testing; From 6fa4cb07c75ce5b02e535a48b9a63e86ccded623 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 13:56:09 -0700 Subject: [PATCH 11/20] more cursor reflow --- src/terminal/Screen.zig | 66 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 2fa93deab..18f242eb0 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -539,8 +539,7 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // 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) { - assert(new_cursor == null); // should only happen once + if (cursor_pos.y == iter.value - 1 and new_cursor == null) { new_cursor = .{ .y = y, .x = cursor_pos.x + x }; } @@ -570,7 +569,6 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { if (cursor_pos.y == iter.value - 1 and cursor_pos.x < new_row.len) { - if (true) @panic("IN FLOW"); // TODO: to test assert(new_cursor == null); // should only happen once new_cursor = .{ .y = y, .x = x + cursor_pos.x }; } @@ -619,6 +617,11 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { 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; @@ -1365,6 +1368,11 @@ test "Screen: resize more cols with reflow that forces more wrapping" { 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); @@ -1381,6 +1389,10 @@ test "Screen: resize more cols with reflow that forces more wrapping" { 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" { @@ -1392,6 +1404,11 @@ test "Screen: resize more cols with reflow that unwraps multiple times" { 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); @@ -1408,6 +1425,10 @@ test "Screen: resize more cols with reflow that unwraps multiple times" { 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 less rows no scrollback" { @@ -1418,8 +1439,47 @@ test "Screen: resize less rows no scrollback" { 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); From e4c8bbd394f7ebaa3783ac904ff39fb8da0c1137 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 14:02:28 -0700 Subject: [PATCH 12/20] reflow cursor when shrinking cols --- src/terminal/Screen.zig | 51 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 18f242eb0..ac027cdef 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -647,6 +647,13 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { ); 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. @@ -658,6 +665,9 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { 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; @@ -672,11 +682,19 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { i -= 1; } + // If our cursor was past the end of this line, move it + // to the end of the contentful area. + if (cursor_pos.y == iter.value - 1 and + cursor_pos.x >= i) + { + cursor_pos.x = i - 1; + } + break :trim row[0..i]; }; // Copy all the cells into our row. - for (trimmed_row) |cell| { + for (trimmed_row) |cell, i| { // Soft wrap if we have to if (x == self.cols) { var last_cell = self.getCell(y, x - 1); @@ -692,6 +710,14 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { 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 = y }; + } + // Copy the old cell, unset the old wrap state var new_cell = self.getCell(y, x); new_cell.* = cell; @@ -709,6 +735,14 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { 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; + } } } @@ -1556,8 +1590,12 @@ test "Screen: resize less cols no reflow" { 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); @@ -1578,8 +1616,13 @@ test "Screen: resize less cols with reflow but row space" { defer s.deinit(alloc); const str = "1ABCD"; s.testWriteString(str); - try s.resize(alloc, 3, 3); + // 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); @@ -1592,6 +1635,10 @@ test "Screen: resize less cols with reflow but row space" { 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" { From 9493561159ab931985a071f155568de71de505ef Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 14:03:14 -0700 Subject: [PATCH 13/20] assertions to save our bacon --- src/terminal/Screen.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index ac027cdef..14f384ac2 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -444,6 +444,13 @@ pub fn copyRow(self: *Screen, dst: usize, src: usize) void { /// 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) { From f3d3d255fb87992c6c0e49476def22d655ff7f1e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 14:08:01 -0700 Subject: [PATCH 14/20] handle case where cursor is past where content is on col shrink --- src/terminal/Screen.zig | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 14f384ac2..13761ecaf 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -689,14 +689,6 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { i -= 1; } - // If our cursor was past the end of this line, move it - // to the end of the contentful area. - if (cursor_pos.y == iter.value - 1 and - cursor_pos.x >= i) - { - cursor_pos.x = i - 1; - } - break :trim row[0..i]; }; @@ -734,6 +726,18 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { 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 = 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 == 0) From a2cf11606379a13880c800b27886ba2e220b138b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 14:20:52 -0700 Subject: [PATCH 15/20] fix a crash when reflowing to smaller cols but still buggy --- src/terminal/Screen.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 13761ecaf..9ff64b995 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -705,7 +705,7 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // If our y is more than our rows, we need to scroll if (y >= self.rows) { self.scroll(.{ .delta = 1 }); - y -= 1; + y = self.rows - 1; x = 0; } @@ -718,6 +718,7 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { } // 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 = 0; From 113b5a318bdb9c727e2c266213eb0105d329b101 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 17:08:57 -0700 Subject: [PATCH 16/20] when shrinking rows, clear empty space from the end (see test case) --- src/terminal/Screen.zig | 93 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 5 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 9ff64b995..84f299b4b 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -608,10 +608,42 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // 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); - const bot_len = @minimum(reg[1].len, storage.len); - const top_len = @minimum(reg[0].len, storage.len - bot_len); - std.mem.copy(Cell, storage, reg[0][reg[0].len - top_len ..]); - std.mem.copy(Cell, storage[top_len..], reg[1][reg[1].len - bot_len ..]); + + // 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 @@ -637,7 +669,6 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // much in the viewport. self.top = 0; self.bottom = @maximum(rows, copied_rows); - //self.bottom = @minimum(self.bottom, copied_rows); //log.warn("bot={} top={} copied={}", .{ bot_len, top_len, copied_rows }); //log.warn("BOTTOM={}", .{self.bottom}); self.scroll(.{ .bottom = {} }); @@ -1700,3 +1731,55 @@ test "Screen: resize less cols with reflow with trimmed rows and scrollback" { 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); + } +} From d551f1126bb90181bbd960c75d401821ad7c814e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 17:44:13 -0700 Subject: [PATCH 17/20] when less cols, cursor needs to be in screen space, not viewport --- src/terminal/Screen.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 84f299b4b..a182ef124 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -745,7 +745,7 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { cursor_pos.x == i) { assert(new_cursor == null); - new_cursor = .{ .x = x, .y = y }; + new_cursor = .{ .x = x, .y = self.visible_offset + y }; } // Copy the old cell, unset the old wrap state @@ -766,7 +766,7 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { assert(new_cursor == null); new_cursor = .{ .x = @minimum(cursor_pos.x, self.cols - 1), - .y = y, + .y = self.visible_offset + y, }; } From 16a5a4529386d39d9e39a5de546f8face249065e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 18:14:11 -0700 Subject: [PATCH 18/20] when growing rows, offset cursor by added rows --- src/terminal/Screen.zig | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index a182ef124..c08a7b289 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -474,12 +474,20 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // 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 @@ -1360,13 +1368,20 @@ test "Screen: resize more rows with populated scrollback" { 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 - const cursor = s.cursor; try s.resize(alloc, 10, 5); try testing.expectEqual(@as(usize, 15), s.totalRows()); - // Cursor should not move - try testing.expectEqual(cursor, s.cursor); + // 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); From f82493cccf0115c31e08368fdb66722bda179580 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 20:52:36 -0700 Subject: [PATCH 19/20] when growing cols, adjust viewport and cursor if we shorten --- src/terminal/Screen.zig | 121 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 110 insertions(+), 11 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index c08a7b289..fb22902f4 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -517,6 +517,9 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { 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; @@ -525,6 +528,13 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { 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 == 0) { y += 1; @@ -535,9 +545,6 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // The goal is to keep the messiness of reflow down here and // only reloop when we're back to clean non-wrapped lines. - // Whether we need to move the cursor or not - var new_cursor: ?point.ScreenPoint = null; - // Mark the last element as not wrapped new_row[row.len - 1].attrs.wrap = 0; @@ -545,7 +552,15 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { var x: usize = row.len; new_row = new_row[x..]; wrapping: while (iter.next()) |wrapped_row| { - var wrapped_rem = 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) { @@ -561,6 +576,17 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { // If this row isn't also wrapped, we're done! if (wrapped_rem[wrapped_rem.len - 1].attrs.wrap == 0) { 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; } @@ -594,14 +620,14 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { 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 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; } } @@ -970,6 +996,10 @@ fn testWriteString(self: *Screen, text: []const u8) void { row[x - 1].attrs.wrap = 1; y += 1; x = 0; + if (y >= self.rows) { + y -= 1; + self.scroll(.{ .delta = 1 }); + } row = self.getRow(.{ .active = y }); } @@ -1451,6 +1481,40 @@ test "Screen: resize more cols with reflow that fits full width" { 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; @@ -1523,6 +1587,41 @@ test "Screen: resize more cols with reflow that unwraps multiple times" { 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; From 38af14ff3ab73dc7e5184c0bfc65ad9ba683d07c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Aug 2022 21:14:27 -0700 Subject: [PATCH 20/20] resize alt screen without reflow --- src/terminal/Screen.zig | 122 ++++++++++++++++++++++++++++++++++++++ src/terminal/Terminal.zig | 12 ++-- 2 files changed, 129 insertions(+), 5 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index fb22902f4..6d0f17419 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -819,10 +819,63 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void { 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 { @@ -1897,3 +1950,72 @@ test "Screen: resize more rows then shrink again" { 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); + } +} diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 6c37a80d9..179ee743c 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -214,8 +214,6 @@ pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) ! const tracy = trace(@src()); defer tracy.end(); - // TODO: test, wrapping, etc. - // If we have deccolm supported then we are fixed at either 80 or 132 // columns depending on if mode 3 is set or not. // TODO: test @@ -232,9 +230,13 @@ pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) ! } // If we're making the screen smaller, dealloc the unused items. - // TODO: reflow - try self.screen.resize(alloc, rows, cols); - try self.secondary_screen.resize(alloc, rows, cols); + if (self.active_screen == .primary) { + try self.screen.resize(alloc, rows, cols); + try self.secondary_screen.resizeWithoutReflow(alloc, rows, cols); + } else { + try self.screen.resizeWithoutReflow(alloc, rows, cols); + try self.secondary_screen.resize(alloc, rows, cols); + } // Set our size self.cols = cols;