terminal: screen scroll with full scrollback modifies selection

This commit is contained in:
Mitchell Hashimoto
2023-03-21 10:59:44 -07:00
parent dfb40426a0
commit 70236ebc33
2 changed files with 202 additions and 58 deletions

View File

@ -1440,7 +1440,8 @@ fn scrollDelta(self: *Screen, delta: isize, grow: bool) !void {
// then we expand out to there. // then we expand out to there.
const rows_written = self.rowsWritten(); const rows_written = self.rowsWritten();
const viewport_bottom = self.viewport + self.rows; const viewport_bottom = self.viewport + self.rows;
if (viewport_bottom > rows_written) { if (viewport_bottom <= rows_written) return;
// The number of new rows we need is the number of rows off our // The number of new rows we need is the number of rows off our
// previous bottom we are growing. // previous bottom we are growing.
const new_rows_needed = viewport_bottom - rows_written; const new_rows_needed = viewport_bottom - rows_written;
@ -1489,6 +1490,23 @@ fn scrollDelta(self: *Screen, delta: isize, grow: bool) !void {
break :deleted rows_to_delete; break :deleted rows_to_delete;
} else 0; } else 0;
// If we are deleting rows and have a selection, then we need to offset
// the selection by the rows we're deleting.
if (self.selection) |*sel| {
// If we're deleting more rows than our Y values, we also move
// the X over to 0 because we're in the middle of the selection now.
if (rows_deleted > sel.start.y) sel.start.x = 0;
if (rows_deleted > sel.end.y) sel.end.x = 0;
// Remove the deleted rows from both y values. We use saturating
// subtraction so that we can detect when we're at zero.
sel.start.y -|= rows_deleted;
sel.end.y -|= rows_deleted;
// If the selection is now empty, just clear it.
if (sel.empty()) self.selection = null;
}
// If we have more rows than what shows on our screen, we have a // If we have more rows than what shows on our screen, we have a
// history boundary. // history boundary.
const rows_written_final = rows_final - rows_deleted; const rows_written_final = rows_final - rows_deleted;
@ -1502,7 +1520,6 @@ fn scrollDelta(self: *Screen, delta: isize, grow: bool) !void {
self.cols + 1, self.cols + 1,
); );
} }
}
/// Returns the raw text associated with a selection. This will unwrap /// Returns the raw text associated with a selection. This will unwrap
/// soft-wrapped edges. The returned slice is owned by the caller and allocated /// soft-wrapped edges. The returned slice is owned by the caller and allocated
@ -2776,6 +2793,128 @@ test "Screen: scrollback empty" {
} }
} }
test "Screen: scrolling moves selection" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5, 0);
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
try testing.expect(s.viewportIsBottom());
// Select a single line
s.selection = .{
.start = .{ .x = 0, .y = 1 },
.end = .{ .x = s.cols - 1, .y = 1 },
};
// Scroll down, should still be bottom
try s.scroll(.{ .delta = 1 });
try testing.expect(s.viewportIsBottom());
// Our selection should've moved up
try testing.expectEqual(Selection{
.start = .{ .x = 0, .y = 0 },
.end = .{ .x = s.cols - 1, .y = 0 },
}, s.selection.?);
{
// Test our contents rotated
var contents = try s.testString(alloc, .viewport);
defer alloc.free(contents);
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
}
// Scrolling to the bottom does nothing
try s.scroll(.{ .bottom = {} });
// Our selection should've stayed the same
try testing.expectEqual(Selection{
.start = .{ .x = 0, .y = 0 },
.end = .{ .x = s.cols - 1, .y = 0 },
}, s.selection.?);
{
// Test our contents rotated
var contents = try s.testString(alloc, .viewport);
defer alloc.free(contents);
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
}
// Scroll up again
try s.scroll(.{ .delta = 1 });
// Our selection should be null because it left the screen.
try testing.expect(s.selection == null);
}
test "Screen: scrolling with scrollback available doesn't move selection" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 3, 5, 1);
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
try testing.expect(s.viewportIsBottom());
// Select a single line
s.selection = .{
.start = .{ .x = 0, .y = 1 },
.end = .{ .x = s.cols - 1, .y = 1 },
};
// Scroll down, should still be bottom
try s.scroll(.{ .delta = 1 });
try testing.expect(s.viewportIsBottom());
// Our selection should NOT move since we have scrollback
try testing.expectEqual(Selection{
.start = .{ .x = 0, .y = 1 },
.end = .{ .x = s.cols - 1, .y = 1 },
}, s.selection.?);
{
// Test our contents rotated
var contents = try s.testString(alloc, .viewport);
defer alloc.free(contents);
try testing.expectEqualStrings("2EFGH\n3IJKL", contents);
}
// Scrolling back should make it visible again
try s.scroll(.{ .delta = -1 });
try testing.expect(!s.viewportIsBottom());
// Our selection should NOT move since we have scrollback
try testing.expectEqual(Selection{
.start = .{ .x = 0, .y = 1 },
.end = .{ .x = s.cols - 1, .y = 1 },
}, s.selection.?);
{
// Test our contents rotated
var contents = try s.testString(alloc, .viewport);
defer alloc.free(contents);
try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents);
}
// Scroll down, this sends us off the scrollback
try s.scroll(.{ .delta = 2 });
try testing.expect(s.viewportIsBottom());
// Selection should move since we went down 2
try testing.expectEqual(Selection{
.start = .{ .x = 0, .y = 0 },
.end = .{ .x = s.cols - 1, .y = 0 },
}, s.selection.?);
{
// Test our contents rotated
var contents = try s.testString(alloc, .viewport);
defer alloc.free(contents);
try testing.expectEqualStrings("3IJKL", contents);
}
}
test "Screen: history region with no scrollback" { test "Screen: history region with no scrollback" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;

View File

@ -33,6 +33,11 @@ pub fn toViewport(self: Selection, screen: *const Screen) ?Selection {
}; };
} }
/// Returns true if the selection is empty.
pub fn empty(self: Selection) bool {
return self.start.x == self.end.x and self.start.y == self.end.y;
}
/// Returns true if the selection contains the given point. /// Returns true if the selection contains the given point.
/// ///
/// This recalculates top left and bottom right each call. If you have /// This recalculates top left and bottom right each call. If you have