terminal: resize less cols attempts to preserve cursor y

Fixes #906

Previously, when the cursor isn't at the bottom and you resized to less
cols, the cursor would jump to the bottom of the viewport. But if you
resized to more columns it didn't do this. This was jarring. This commit
attempts to keep the cursor at the same place.
This commit is contained in:
Mitchell Hashimoto
2023-11-20 13:29:24 -08:00
parent 9cc7a362e3
commit db7262a8dd

View File

@ -2427,6 +2427,9 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void {
// No matter what we mark our image state as dirty
self.kitty_images.dirty = true;
// Keep track if our cursor is at the bottom
const cursor_bottom = self.cursor.y == self.rows - 1;
// 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) {
@ -2667,6 +2670,7 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void {
// Whether we need to move the cursor or not
var new_cursor: ?point.ScreenPoint = null;
var new_cursor_wrap: usize = 0;
// Reset our variables because we're going to reprint the screen.
self.cols = cols;
@ -2767,6 +2771,13 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void {
}
}
if (cursor_pos.y == old_y) {
// If this original y is where our cursor is, we
// track the number of wraps we do so we can try to
// keep this whole line on the screen.
new_cursor_wrap += 1;
}
row = self.getRow(.{ .active = y });
row.setSemanticPrompt(cur_old_row.getSemanticPrompt());
}
@ -2812,6 +2823,32 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void {
const viewport_pos = pos.toViewport(self);
self.cursor.x = @min(viewport_pos.x, self.cols - 1);
self.cursor.y = @min(viewport_pos.y, self.rows - 1);
// We want to keep our cursor y at the same place. To do so, we
// scroll the screen. This scrolls all of the content so the cell
// the cursor is over doesn't change.
if (!cursor_bottom and old.cursor.y < self.cursor.y) scroll: {
const delta: isize = delta: {
var delta: isize = @intCast(self.cursor.y - old.cursor.y);
// new_cursor_wrap is the number of times the line that the
// cursor was on previously was wrapped to fit this new col
// width. We want to scroll that many times less so that
// the whole line the cursor was on attempts to remain
// in view.
delta -= @intCast(new_cursor_wrap);
if (delta <= 0) break :scroll;
break :delta delta;
};
self.scroll(.{ .screen = delta }) catch |err| {
log.warn("failed to scroll for resize, cursor may be off err={}", .{err});
break :scroll;
};
self.cursor.y -= @intCast(delta);
}
} else {
// TODO: why is this necessary? Without this, neovim will
// crash when we shrink the window to the smallest size. We
@ -6421,6 +6458,36 @@ test "Screen: resize less cols with reflow previously wrapped and scrollback" {
try testing.expectEqual(@as(usize, 2), s.cursor.y);
}
test "Screen: resize less cols with scrollback keeps cursor row" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5, 5);
defer s.deinit();
const str = "1A\n2B\n3C\n4D\n5E";
try s.testWriteString(str);
// Lets do a scroll and clear operation
try s.scroll(.{ .clear = {} });
// Move our cursor to the beginning
s.cursor.x = 0;
s.cursor.y = 0;
try s.resize(3, 3);
{
const contents = try s.testString(alloc, .viewport);
defer alloc.free(contents);
const expected = "";
try testing.expectEqualStrings(expected, contents);
}
// Cursor should be on the last line
try testing.expectEqual(@as(usize, 0), s.cursor.x);
try testing.expectEqual(@as(usize, 0), s.cursor.y);
}
test "Screen: resize more rows, less cols with reflow with scrollback" {
const testing = std.testing;
const alloc = testing.allocator;