diff --git a/src/terminal/new/PageList.zig b/src/terminal/new/PageList.zig index ea31a3e8a..6c6b89f0f 100644 --- a/src/terminal/new/PageList.zig +++ b/src/terminal/new/PageList.zig @@ -357,6 +357,16 @@ pub const Resize = struct { /// Whether to reflow the text. If this is false then the text will /// be truncated if the new size is smaller than the old size. reflow: bool = true, + + /// Set this to a cursor position and the resize will retain the + /// cursor position and update this so that the cursor remains over + /// the same original cell in the reflowed environment. + cursor: ?*Cursor = null, + + pub const Cursor = struct { + x: size.CellCountInt, + y: size.CellCountInt, + }; }; /// Resize @@ -614,7 +624,15 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { // behavior because it seemed fine in an ocean of differing behavior // between terminal apps. I'm completely open to changing it as long // as resize behavior isn't regressed in a user-hostile way. - _ = self.trimTrailingBlankRows(self.rows - rows); + const trimmed = self.trimTrailingBlankRows(self.rows - rows); + + // If we have a cursor, we want to preserve the y value as + // best we can. We need to subtract the number of rows that + // moved into the scrollback. + if (opts.cursor) |cursor| { + const scrollback = self.rows - rows - trimmed; + cursor.y -|= scrollback; + } // If we didn't trim enough, just modify our row count and this // will create additional history. @@ -624,20 +642,45 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { // Making rows larger we adjust our row count, and then grow // to the row count. .gt => gt: { - self.rows = rows; + // If our rows increased and our cursor is NOT at the bottom, + // we want to try to preserve the y value of the old cursor. + // In other words, we don't want to "pull down" scrollback. + // This is purely a UX feature. + if (opts.cursor) |cursor| cursor: { + if (cursor.y >= self.rows - 1) break :cursor; - // Perform a quick count to make sure we have at least - // the number of rows we need. This should be fast because - // we only need to count up to "rows" + // Cursor is not at the bottom, so we just grow our + // rows and we're done. Cursor does NOT change for this + // since we're not pulling down scrollback. + for (0..rows - self.rows) |_| _ = try self.grow(); + self.rows = rows; + break :gt; + } + + // Cursor is at the bottom or we don't care about cursors. + // In this case, if we have enough rows in our pages, we + // just update our rows and we're done. This effectively + // "pulls down" scrollback. + // + // If we don't have enough scrollback, we add the difference, + // to the active area. var count: usize = 0; var page = self.pages.first; while (page) |p| : (page = p.next) { count += p.data.size.rows; - if (count >= rows) break :gt; + if (count >= rows) break; + } else { + assert(count < rows); + for (count..rows) |_| _ = try self.grow(); } - assert(count < rows); - for (count..rows) |_| _ = try self.grow(); + // Update our cursor. W + if (opts.cursor) |cursor| { + const grow_len: size.CellCountInt = @intCast(rows -| count); + cursor.y += rows - self.rows - grow_len; + } + + self.rows = rows; }, } } @@ -663,6 +706,11 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { page.size.cols = cols; } + if (opts.cursor) |cursor| { + // If our cursor is off the edge we trimmed, update to edge + if (cursor.x >= cols) cursor.x = cols - 1; + } + self.cols = cols; }, @@ -2129,11 +2177,19 @@ test "PageList resize (no reflow) more rows" { defer s.deinit(); try testing.expectEqual(@as(usize, 3), s.totalRows()); + // Cursor is at the bottom + var cursor: Resize.Cursor = .{ .x = 0, .y = 2 }; + // Resize - try s.resize(.{ .rows = 10, .reflow = false }); + try s.resize(.{ .rows = 10, .reflow = false, .cursor = &cursor }); try testing.expectEqual(@as(usize, 10), s.rows); try testing.expectEqual(@as(usize, 10), s.totalRows()); + // Our cursor should not move because we have no scrollback so + // we just grew. + try testing.expectEqual(@as(size.CellCountInt, 0), cursor.x); + try testing.expectEqual(@as(size.CellCountInt, 2), cursor.y); + { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ @@ -2158,10 +2214,18 @@ test "PageList resize (no reflow) more rows with history" { } }, pt); } + // Cursor is at the bottom + var cursor: Resize.Cursor = .{ .x = 0, .y = 2 }; + // Resize - try s.resize(.{ .rows = 5, .reflow = false }); + try s.resize(.{ .rows = 5, .reflow = false, .cursor = &cursor }); try testing.expectEqual(@as(usize, 5), s.rows); try testing.expectEqual(@as(usize, 53), s.totalRows()); + + // Our cursor should move since it's in the scrollback + try testing.expectEqual(@as(size.CellCountInt, 0), cursor.x); + try testing.expectEqual(@as(size.CellCountInt, 4), cursor.y); + { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ @@ -2205,6 +2269,55 @@ test "PageList resize (no reflow) less rows" { } } +test "PageList resize (no reflow) less rows cursor in scrollback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + try testing.expectEqual(@as(usize, 10), s.totalRows()); + + // This is required for our writing below to work + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + // Write into all rows so we don't get trim behavior + for (0..s.rows) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(y) }, + }; + } + + // Let's say our cursor is in the scrollback + var cursor: Resize.Cursor = .{ .x = 0, .y = 2 }; + { + const get = s.getCell(.{ .active = .{ + .x = cursor.x, + .y = cursor.y, + } }).?; + try testing.expectEqual(@as(u21, 2), get.cell.content.codepoint); + } + + // Resize + try s.resize(.{ .rows = 5, .reflow = false, .cursor = &cursor }); + try testing.expectEqual(@as(usize, 5), s.rows); + try testing.expectEqual(@as(usize, 10), s.totalRows()); + + // Our cursor should move since it's in the scrollback + try testing.expectEqual(@as(size.CellCountInt, 0), cursor.x); + try testing.expectEqual(@as(size.CellCountInt, 0), cursor.y); + + { + const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 5, + } }, pt); + } +} + test "PageList resize (no reflow) less rows trims blank lines" { const testing = std.testing; const alloc = testing.allocator; @@ -2232,10 +2345,25 @@ test "PageList resize (no reflow) less rows trims blank lines" { }; } + // Let's say our cursor is at the top + var cursor: Resize.Cursor = .{ .x = 0, .y = 0 }; + { + const get = s.getCell(.{ .active = .{ + .x = cursor.x, + .y = cursor.y, + } }).?; + try testing.expectEqual(@as(u21, 'A'), get.cell.content.codepoint); + } + // Resize - try s.resize(.{ .rows = 2, .reflow = false }); + try s.resize(.{ .rows = 2, .reflow = false, .cursor = &cursor }); try testing.expectEqual(@as(usize, 2), s.rows); try testing.expectEqual(@as(usize, 2), s.totalRows()); + + // Our cursor should not move since we trimmed + try testing.expectEqual(@as(size.CellCountInt, 0), cursor.x); + try testing.expectEqual(@as(size.CellCountInt, 0), cursor.y); + { const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ @@ -2481,6 +2609,74 @@ test "PageList resize (no reflow) more cols forces smaller cap" { } } +test "PageList resize (no reflow) more rows adds blank rows if cursor at bottom" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 5, 3, null); + defer s.deinit(); + + // Grow to 5 total rows, simulating 3 active + 2 scrollback + try s.growRows(2); + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + for (0..s.totalRows()) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(y) }, + }; + } + + // Active should be on row 3 + { + const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 2, + } }, pt); + } + + // Let's say our cursor is at the bottom + var cursor: Resize.Cursor = .{ .x = 0, .y = s.rows - 2 }; + { + const get = s.getCell(.{ .active = .{ + .x = cursor.x, + .y = cursor.y, + } }).?; + try testing.expectEqual(@as(u21, 3), get.cell.content.codepoint); + } + + // Resize + const original_cursor = cursor; + try s.resizeWithoutReflow(.{ .rows = 10, .reflow = false, .cursor = &cursor }); + try testing.expectEqual(@as(usize, 5), s.cols); + try testing.expectEqual(@as(usize, 10), s.rows); + + // Our cursor should not change + try testing.expectEqual(original_cursor, cursor); + + // 12 because we have our 10 rows in the active + 2 in the scrollback + // because we're preserving the cursor. + try testing.expectEqual(@as(usize, 12), s.totalRows()); + + // Active should be at the same place it was. + { + const pt = s.getCell(.{ .active = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 2, + } }, pt); + } + + // Go through our active, we should get only 3,4,5 + for (0..3) |y| { + const get = s.getCell(.{ .active = .{ .y = y } }).?; + const expected: u21 = @intCast(y + 2); + try testing.expectEqual(expected, get.cell.content.codepoint); + } +} + test "PageList resize reflow more cols no wrapped rows" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index c894fdebd..6197f0eb8 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -617,7 +617,21 @@ pub fn resizeWithoutReflow( cols: size.CellCountInt, rows: size.CellCountInt, ) !void { - try self.pages.resize(.{ .rows = rows, .cols = cols, .reflow = false }); + var cursor: PageList.Resize.Cursor = .{ + .x = self.cursor.x, + .y = self.cursor.y, + }; + + try self.pages.resize(.{ + .rows = rows, + .cols = cols, + .reflow = false, + .cursor = &cursor, + }); + + self.cursor.x = cursor.x; + self.cursor.y = cursor.y; + self.cursorReload(); } /// Set a style attribute for the current cursor. @@ -1853,8 +1867,14 @@ test "Screen: resize (no reflow) less rows" { defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str); + try testing.expectEqual(5, s.cursor.x); + try testing.expectEqual(2, s.cursor.y); try s.resizeWithoutReflow(10, 2); + // Since we shrunk, we should adjust our cursor + try testing.expectEqual(5, s.cursor.x); + try testing.expectEqual(1, s.cursor.y); + { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); @@ -2139,21 +2159,20 @@ test "Screen: resize more rows with populated scrollback" { try s.resize(5, 10); // Cursor should still be on the "4" - // TODO - // { - // const list_cell = s.pages.getCell(.{ .active = .{ - // .x = s.cursor.x, - // .y = s.cursor.y, - // } }).?; - // try testing.expectEqual(@as(u21, '4'), list_cell.cell.content.codepoint); - // } + { + const list_cell = s.pages.getCell(.{ .active = .{ + .x = s.cursor.x, + .y = s.cursor.y, + } }).?; + try testing.expectEqual(@as(u21, '4'), list_cell.cell.content.codepoint); + } - // { - // const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); - // defer alloc.free(contents); - // const expected = "3IJKL\n4ABCD\n5EFGH"; - // try testing.expectEqualStrings(expected, contents); - // } + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + const expected = "3IJKL\n4ABCD\n5EFGH"; + try testing.expectEqualStrings(expected, contents); + } } // test "Screen: resize more cols no reflow" {