From e0867801a5aff54a706d99ac52d82944d058b551 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 11 Jan 2024 20:58:12 -0500 Subject: [PATCH 1/4] Added use of shift+arrow keys to expand current selection --- src/Surface.zig | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 0a7be6aa2..29cef241d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1264,6 +1264,86 @@ pub fn keyCallback( } } + // Expand selection if one exists and event is shift + + if (event.mods.shift) adjust_selection: { + 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; + + // Silently consume key releases. + if (event.action != .press and event.action != .repeat) return .consumed; + + 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.physical_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.y -= 1; + }, + .down => { + if (sel.end.y < screen_end) sel.end.y += 1; + }, + else => { break :adjust_selection; }, + } + // 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)) + }); + } + self.setSelection(sel); + try self.queueRender(); + return .consumed; + } + // If we allow KAM and KAM is enabled then we do nothing. if (self.config.vt_kam_allowed) { self.renderer_state.mutex.lock(); From 8cf3b6bef3077a2b06d556732a3f58c387ea540f Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 11 Jan 2024 21:19:41 -0500 Subject: [PATCH 2/4] Shift+keys selection: Fixed up/down logic, added page up/down, home, & end. --- src/Surface.zig | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 29cef241d..2277cfc7d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1281,7 +1281,7 @@ pub fn keyCallback( const viewport_end = screen.viewport + terminal.Screen.RowIndexTag.viewport.maxLen(&screen) - 1; const screen_end = terminal.Screen.RowIndexTag.screen.maxLen(&screen) - 1; - switch (event.physical_key) { + switch (event.key) { .left => { var iterator = sel.end.iterator(&screen, .left_up); // This iterator emits the start point first, throw it out. @@ -1322,10 +1322,43 @@ pub fn keyCallback( } }, .up => { - if (sel.end.y > 0) sel.end.y -= 1; + if (sel.end.y == 0) { + sel.end.x = 0; + } else { + sel.end.y -= 1; + } }, .down => { - if (sel.end.y < screen_end) sel.end.y += 1; + 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; }, } From 3f3703deb660d1a300d5e9425c9ec4bd6f2c734f Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 11 Jan 2024 21:58:19 -0500 Subject: [PATCH 3/4] =?UTF-8?q?Fixed=20accidentally=20consuming=20ALL=20sh?= =?UTF-8?q?ift+=20releases=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Surface.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 2277cfc7d..9ffd6f0b2 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1273,9 +1273,6 @@ pub fn keyCallback( if (selection == null) break :adjust_selection; - // Silently consume key releases. - if (event.action != .press and event.action != .repeat) return .consumed; - var sel = selection.?; const viewport_end = screen.viewport + terminal.Screen.RowIndexTag.viewport.maxLen(&screen) - 1; @@ -1362,6 +1359,10 @@ pub fn keyCallback( }, 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(.{ From 50a119d30090187b8b643cce4f22ab9bd627fcab Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Jan 2024 22:14:40 -0800 Subject: [PATCH 4/4] Selection: add adjust method, unit test it, swap for adjustments --- src/Surface.zig | 128 ++++----------- src/terminal/Selection.zig | 315 +++++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+), 99 deletions(-) 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; {