Selection: add adjust method, unit test it, swap for adjustments

This commit is contained in:
Mitchell Hashimoto
2024-01-11 22:14:40 -08:00
parent 3f3703deb6
commit 50a119d300
2 changed files with 344 additions and 99 deletions

View File

@ -1269,110 +1269,40 @@ pub fn keyCallback(
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
var screen = self.io.terminal.screen;
const selection = screen.selection;
if (selection == null) break :adjust_selection;
var sel = selection.?;
const viewport_end = screen.viewport + terminal.Screen.RowIndexTag.viewport.maxLen(&screen) - 1;
const screen_end = terminal.Screen.RowIndexTag.screen.maxLen(&screen) - 1;
switch (event.key) {
.left => {
var iterator = sel.end.iterator(&screen, .left_up);
// This iterator emits the start point first, throw it out.
_ = iterator.next();
var next = iterator.next();
// Step left, wrapping to the next row up at the start of each new line,
// until we find a non-empty cell.
while (
next != null and
screen.getCell(.screen, next.?.y, next.?.x).char == 0
) {
next = iterator.next();
}
if (next != null) {
sel.end = next.?;
}
},
.right => {
var iterator = sel.end.iterator(&screen, .right_down);
// This iterator emits the start point first, throw it out.
_ = iterator.next();
var next = iterator.next();
// Step right, wrapping to the next row down at the start of each new line,
// until we find a non-empty cell.
while (
next != null and
next.?.y <= screen_end and
screen.getCell(.screen, next.?.y, next.?.x).char == 0
) {
next = iterator.next();
}
if (next != null) {
if (next.?.y > screen_end) {
sel.end.y = screen_end;
} else {
sel.end = next.?;
}
}
},
.up => {
if (sel.end.y == 0) {
sel.end.x = 0;
} else {
sel.end.y -= 1;
}
},
.down => {
if (sel.end.y >= screen_end) {
sel.end.y = screen_end;
sel.end.x = screen.cols - 1;
} else {
sel.end.y += 1;
}
},
.page_up => {
if (screen.rows > sel.end.y) {
sel.end.y = 0;
sel.end.x = 0;
} else {
sel.end.y -= screen.rows;
}
},
.page_down => {
if (screen.rows > screen_end - sel.end.y) {
sel.end.y = screen_end;
sel.end.x = screen.cols - 1;
} else {
sel.end.y += screen.rows;
}
},
.home => {
sel.end.y = 0;
sel.end.x = 0;
},
.end => {
sel.end.y = screen_end;
sel.end.x = screen.cols - 1;
},
else => { break :adjust_selection; },
}
const sel = sel: {
const old_sel = screen.selection orelse break :adjust_selection;
break :sel old_sel.adjust(&screen, switch (event.key) {
.left => .left,
.right => .right,
.up => .up,
.down => .down,
.page_up => .page_up,
.page_down => .page_down,
.home => .home,
.end => .end,
else => break :adjust_selection,
});
};
// Silently consume key releases.
if (event.action != .press and event.action != .repeat) return .consumed;
// If the selection endpoint is outside of the current viewpoint, scroll it in to view.
if (sel.end.y < screen.viewport) {
try self.io.terminal.scrollViewport(.{
.delta = @as(isize, @intCast(sel.end.y)) - @as(isize, @intCast(screen.viewport))
});
} else if (sel.end.y > viewport_end) {
try self.io.terminal.scrollViewport(.{
.delta = @as(isize, @intCast(sel.end.y)) - @as(isize, @intCast(viewport_end))
});
// If the selection endpoint is outside of the current viewpoint,
// scroll it in to view.
scroll: {
const viewport_max = terminal.Screen.RowIndexTag.viewport.maxLen(&screen) - 1;
const viewport_end = screen.viewport + viewport_max;
const delta: isize = if (sel.end.y < screen.viewport)
@intCast(screen.viewport)
else if (sel.end.y > viewport_end)
@intCast(viewport_end)
else
break :scroll;
const start_y: isize = @intCast(sel.end.y);
try self.io.terminal.scrollViewport(.{ .delta = start_y - delta });
}
// Change our selection and queue a render so its shown.
self.setSelection(sel);
try self.queueRender();
return .consumed;

View File

@ -222,6 +222,321 @@ pub fn order(self: Selection) Order {
return .reverse;
}
/// Possible adjustments to the selection.
pub const Adjustment = enum {
left,
right,
up,
down,
home,
end,
page_up,
page_down,
};
/// Adjust the selection by some given adjustment. An adjustment allows
/// a selection to be expanded slightly left, right, up, down, etc.
pub fn adjust(self: Selection, screen: *Screen, adjustment: Adjustment) Selection {
const screen_end = Screen.RowIndexTag.screen.maxLen(screen) - 1;
// Make an editable one because its so much easier to use modification
// logic below than it is to reconstruct the selection every time.
var result = self;
// Note that we always adjusts "end" because end always represents
// the last point of the selection by mouse, not necessarilly the
// top/bottom visually. So this results in the right behavior
// whether the user drags up or down.
switch (adjustment) {
.up => if (result.end.y == 0) {
result.end.x = 0;
} else {
result.end.y -= 1;
},
.down => if (result.end.y >= screen_end) {
result.end.y = screen_end;
result.end.x = screen.cols - 1;
} else {
result.end.y += 1;
},
.left => {
// Step left, wrapping to the next row up at the start of each new line,
// until we find a non-empty cell.
//
// This iterator emits the start point first, throw it out.
var iterator = result.end.iterator(screen, .left_up);
_ = iterator.next();
while (iterator.next()) |next| {
if (screen.getCell(
.screen,
next.y,
next.x,
).char != 0) {
result.end = next;
break;
}
}
},
.right => {
// Step right, wrapping to the next row down at the start of each new line,
// until we find a non-empty cell.
var iterator = result.end.iterator(screen, .right_down);
_ = iterator.next();
while (iterator.next()) |next| {
if (next.y > screen_end) break;
if (screen.getCell(
.screen,
next.y,
next.x,
).char != 0) {
if (next.y > screen_end) {
result.end.y = screen_end;
} else {
result.end = next;
}
break;
}
}
},
.page_up => if (screen.rows > result.end.y) {
result.end.y = 0;
result.end.x = 0;
} else {
result.end.y -= screen.rows;
},
.page_down => if (screen.rows > screen_end - result.end.y) {
result.end.y = screen_end;
result.end.x = screen.cols - 1;
} else {
result.end.y += screen.rows;
},
.home => {
result.end.y = 0;
result.end.x = 0;
},
.end => {
result.end.y = screen_end;
result.end.x = screen.cols - 1;
},
}
return result;
}
test "Selection: adjust right" {
const testing = std.testing;
var screen = try Screen.init(testing.allocator, 5, 10, 0);
defer screen.deinit();
try screen.testWriteString("A1234\nB5678\nC1234\nD5678");
// Simple movement right
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 3, .y = 3 },
}).adjust(&screen, .right);
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 4, .y = 3 },
}, sel);
}
// Already at end of the line.
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 4, .y = 2 },
}).adjust(&screen, .right);
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 0, .y = 3 },
}, sel);
}
// Already at end of the screen
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 4, .y = 3 },
}).adjust(&screen, .right);
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 4, .y = 3 },
}, sel);
}
}
test "Selection: adjust left" {
const testing = std.testing;
var screen = try Screen.init(testing.allocator, 5, 10, 0);
defer screen.deinit();
try screen.testWriteString("A1234\nB5678\nC1234\nD5678");
// Simple movement left
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 3, .y = 3 },
}).adjust(&screen, .left);
// Start line
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 2, .y = 3 },
}, sel);
}
// Already at beginning of the line.
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 0, .y = 3 },
}).adjust(&screen, .left);
// Start line
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 4, .y = 2 },
}, sel);
}
}
test "Selection: adjust left skips blanks" {
const testing = std.testing;
var screen = try Screen.init(testing.allocator, 5, 10, 0);
defer screen.deinit();
try screen.testWriteString("A1234\nB5678\nC12\nD56");
// Same line
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 4, .y = 3 },
}).adjust(&screen, .left);
// Start line
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 2, .y = 3 },
}, sel);
}
// Edge
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 0, .y = 3 },
}).adjust(&screen, .left);
// Start line
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 2, .y = 2 },
}, sel);
}
}
test "Selection: adjust up" {
const testing = std.testing;
var screen = try Screen.init(testing.allocator, 5, 10, 0);
defer screen.deinit();
try screen.testWriteString("A\nB\nC\nD\nE");
// Not on the first line
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 3, .y = 3 },
}).adjust(&screen, .up);
// Start line
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 3, .y = 2 },
}, sel);
}
// On the first line
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 3, .y = 0 },
}).adjust(&screen, .up);
// Start line
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 0, .y = 0 },
}, sel);
}
}
test "Selection: adjust down" {
const testing = std.testing;
var screen = try Screen.init(testing.allocator, 5, 10, 0);
defer screen.deinit();
try screen.testWriteString("A\nB\nC\nD\nE");
// Not on the first line
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 3, .y = 3 },
}).adjust(&screen, .down);
// Start line
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 3, .y = 4 },
}, sel);
}
// On the last line
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 3, .y = 4 },
}).adjust(&screen, .down);
// Start line
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 9, .y = 4 },
}, sel);
}
}
test "Selection: adjust down with not full screen" {
const testing = std.testing;
var screen = try Screen.init(testing.allocator, 5, 10, 0);
defer screen.deinit();
try screen.testWriteString("A\nB\nC");
// On the last line
{
const sel = (Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 3, .y = 2 },
}).adjust(&screen, .down);
// Start line
try testing.expectEqual(Selection{
.start = .{ .x = 5, .y = 1 },
.end = .{ .x = 9, .y = 2 },
}, sel);
}
}
test "Selection: contains" {
const testing = std.testing;
{