diff --git a/src/Surface.zig b/src/Surface.zig index 9ffd6f0b2..024670cfc 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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; diff --git a/src/terminal/Selection.zig b/src/terminal/Selection.zig index b1053afc8..2873313c9 100644 --- a/src/terminal/Selection.zig +++ b/src/terminal/Selection.zig @@ -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; {