pull/11768/merge
Emre Sırmalı 2026-06-03 14:35:18 +08:00 committed by GitHub
commit 8f1ea0dac5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 340 additions and 25 deletions

View File

@ -5565,7 +5565,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.end => .end,
.beginning_of_line => .beginning_of_line,
.end_of_line => .end_of_line,
});
.word_left => .word_left,
.word_right => .word_right,
}, self.config.selection_word_chars);
// If the selection endpoint is outside of the current viewpoint,
// scroll it in to view. Note we always specifically use sel.end

View File

@ -6594,6 +6594,18 @@ pub const Keybinds = struct {
.{ .adjust_selection = .end },
.{ .performable = true },
);
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_left }, .mods = .{ .shift = true, .alt = true } },
.{ .adjust_selection = .word_left },
.{ .performable = true },
);
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_right }, .mods = .{ .shift = true, .alt = true } },
.{ .adjust_selection = .word_right },
.{ .performable = true },
);
// Tabs common to all platforms
try self.set.put(

View File

@ -505,6 +505,11 @@ pub const Action = union(enum) {
/// Adjust the selection to the beginning or the end of the line
/// respectively.
///
/// - `word_left`, `word_right`
///
/// Adjust the selection one word to the left or right respectively,
/// using the configured `selection-word-chars` for word boundaries.
///
adjust_selection: AdjustSelection,
/// Jump the viewport forward or back by the given number of prompts.
@ -1028,6 +1033,8 @@ pub const Action = union(enum) {
end,
beginning_of_line,
end_of_line,
word_left,
word_right,
};
pub const SplitDirection = enum {

View File

@ -414,6 +414,7 @@ pub fn adjust(
self: *Selection,
s: *const Screen,
adjustment: Adjustment,
boundary_codepoints: ?[]const u21,
) void {
// Note that we always adjust "end" because end always represents
// the last point of the selection by mouse, not necessarily the
@ -424,7 +425,7 @@ pub fn adjust(
.up => if (end_pin.up(1)) |new_end| {
end_pin.* = new_end;
} else {
self.adjust(s, .beginning_of_line);
self.adjust(s, .beginning_of_line, null);
},
.down => {
@ -439,7 +440,7 @@ pub fn adjust(
}
} else {
// If we're at the bottom, just go to the end of the line
self.adjust(s, .end_of_line);
self.adjust(s, .end_of_line, null);
}
},
@ -472,13 +473,13 @@ pub fn adjust(
.page_up => if (end_pin.up(s.pages.rows)) |new_end| {
end_pin.* = new_end;
} else {
self.adjust(s, .home);
self.adjust(s, .home, null);
},
.page_down => if (end_pin.down(s.pages.rows)) |new_end| {
end_pin.* = new_end;
} else {
self.adjust(s, .end);
self.adjust(s, .end, null);
},
.home => end_pin.* = s.pages.pin(.{ .screen = .{
@ -506,6 +507,118 @@ pub fn adjust(
.beginning_of_line => end_pin.x = 0,
.end_of_line => end_pin.x = end_pin.node.data.size.cols - 1,
.word_left => {
const codepoints = boundary_codepoints orelse return;
var it = end_pin.cellIterator(.left_up, null);
_ = it.next(); // skip current cell
// Phase 1: skip boundary characters (whitespace/punctuation) going left.
// Empty cells and hard line breaks (non-wrapped rows) are treated
// as hard boundaries, consistent with selectWord in Screen.zig.
var last_non_boundary: ?Pin = null;
while (it.next()) |next| {
const rac = next.rowAndCell();
const cell = rac.cell;
// Stop at hard line breaks (landing on last col of a non-wrapped row)
if (next.x == next.node.data.size.cols - 1 and !rac.row.wrap) break;
// Empty cells are hard boundaries
if (!cell.hasText()) break;
const is_boundary = std.mem.indexOfAny(
u21,
codepoints,
&[_]u21{cell.content.codepoint},
) != null;
if (!is_boundary) {
last_non_boundary = next;
break;
}
}
// Phase 2: skip non-boundary characters (word body) going left
// to find the start of the word.
if (last_non_boundary != null) {
while (it.next()) |next| {
const rac = next.rowAndCell();
const cell = rac.cell;
// Stop at hard line breaks
if (next.x == next.node.data.size.cols - 1 and !rac.row.wrap) break;
if (!cell.hasText()) break;
const is_boundary = std.mem.indexOfAny(
u21,
codepoints,
&[_]u21{cell.content.codepoint},
) != null;
if (is_boundary) break;
last_non_boundary = next;
}
}
if (last_non_boundary) |pos| {
end_pin.* = pos;
}
},
.word_right => {
const codepoints = boundary_codepoints orelse return;
var it = end_pin.cellIterator(.right_down, null);
_ = it.next(); // skip current cell
// Phase 1: skip boundary characters (whitespace/punctuation) going right.
// Empty cells and hard line breaks (non-wrapped rows) are treated
// as hard boundaries, consistent with selectWord in Screen.zig.
var last_non_boundary: ?Pin = null;
while (it.next()) |next| {
const rac = next.rowAndCell();
const cell = rac.cell;
// Stop at hard line breaks
if (next.x == next.node.data.size.cols - 1 and !rac.row.wrap) break;
// Empty cells are hard boundaries
if (!cell.hasText()) break;
const is_boundary = std.mem.indexOfAny(
u21,
codepoints,
&[_]u21{cell.content.codepoint},
) != null;
if (!is_boundary) {
last_non_boundary = next;
break;
}
}
// Phase 2: skip non-boundary characters (word body) going right
// to find the end of the word.
if (last_non_boundary != null) {
while (it.next()) |next| {
const rac = next.rowAndCell();
const cell = rac.cell;
// Stop at hard line breaks
if (next.x == next.node.data.size.cols - 1 and !rac.row.wrap) break;
if (!cell.hasText()) break;
const is_boundary = std.mem.indexOfAny(
u21,
codepoints,
&[_]u21{cell.content.codepoint},
) != null;
if (is_boundary) break;
last_non_boundary = next;
}
}
if (last_non_boundary) |pos| {
end_pin.* = pos;
}
},
}
}
@ -523,7 +636,7 @@ test "Selection: adjust right" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .right);
sel.adjust(&s, .right, null);
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
@ -543,7 +656,7 @@ test "Selection: adjust right" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .right);
sel.adjust(&s, .right, null);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
@ -563,7 +676,7 @@ test "Selection: adjust right" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .right);
sel.adjust(&s, .right, null);
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
@ -590,7 +703,7 @@ test "Selection: adjust left" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .left);
sel.adjust(&s, .left, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -611,7 +724,7 @@ test "Selection: adjust left" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .left);
sel.adjust(&s, .left, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -639,7 +752,7 @@ test "Selection: adjust left skips blanks" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .left);
sel.adjust(&s, .left, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -660,7 +773,7 @@ test "Selection: adjust left skips blanks" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .left);
sel.adjust(&s, .left, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -688,7 +801,7 @@ test "Selection: adjust up" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .up);
sel.adjust(&s, .up, null);
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
@ -708,7 +821,7 @@ test "Selection: adjust up" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .up);
sel.adjust(&s, .up, null);
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
@ -735,7 +848,7 @@ test "Selection: adjust down" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .down);
sel.adjust(&s, .down, null);
try testing.expectEqual(point.Point{ .screen = .{
.x = 5,
@ -755,7 +868,7 @@ test "Selection: adjust down" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .down);
sel.adjust(&s, .down, null);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
@ -782,7 +895,7 @@ test "Selection: adjust down with not full screen" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .down);
sel.adjust(&s, .down, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -810,7 +923,7 @@ test "Selection: adjust home" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .home);
sel.adjust(&s, .home, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -838,7 +951,7 @@ test "Selection: adjust end with not full screen" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .end);
sel.adjust(&s, .end, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -866,7 +979,7 @@ test "Selection: adjust beginning of line" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .beginning_of_line);
sel.adjust(&s, .beginning_of_line, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -887,7 +1000,7 @@ test "Selection: adjust beginning of line" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .beginning_of_line);
sel.adjust(&s, .beginning_of_line, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -908,7 +1021,7 @@ test "Selection: adjust beginning of line" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .beginning_of_line);
sel.adjust(&s, .beginning_of_line, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -936,7 +1049,7 @@ test "Selection: adjust end of line" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .end_of_line);
sel.adjust(&s, .end_of_line, null);
try testing.expectEqual(point.Point{ .screen = .{
.x = 1,
@ -956,7 +1069,7 @@ test "Selection: adjust end of line" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .end_of_line);
sel.adjust(&s, .end_of_line, null);
try testing.expectEqual(point.Point{ .screen = .{
.x = 1,
@ -976,7 +1089,7 @@ test "Selection: adjust end of line" {
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .end_of_line);
sel.adjust(&s, .end_of_line, null);
// Start line
try testing.expectEqual(point.Point{ .screen = .{
@ -990,6 +1103,187 @@ test "Selection: adjust end of line" {
}
}
test "Selection: adjust word_right" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, .{ .cols = 20, .rows = 10, .max_scrollback = 0 });
defer s.deinit();
try s.testWriteString("hello world foo bar");
const boundary = &[_]u21{ ' ', '\t' };
// word_right from middle of "hello" -> end of "hello"
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .word_right, boundary);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// word_right from end of "hello" -> end of "world"
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?,
s.pages.pin(.{ .screen = .{ .x = 4, .y = 0 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .word_right, boundary);
try testing.expectEqual(point.Point{ .screen = .{
.x = 10,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// word_right from last word "bar" -> end of "bar"
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?,
s.pages.pin(.{ .screen = .{ .x = 16, .y = 0 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .word_right, boundary);
try testing.expectEqual(point.Point{ .screen = .{
.x = 18,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: adjust word_right with multiple spaces" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, .{ .cols = 20, .rows = 10, .max_scrollback = 0 });
defer s.deinit();
try s.testWriteString("hello world");
const boundary = &[_]u21{ ' ', '\t' };
// word_right from middle of "hello" -> end of "hello"
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .word_right, boundary);
try testing.expectEqual(point.Point{ .screen = .{
.x = 4,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: adjust word_left" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, .{ .cols = 20, .rows = 10, .max_scrollback = 0 });
defer s.deinit();
try s.testWriteString("hello world foo bar");
const boundary = &[_]u21{ ' ', '\t' };
// word_left from middle of "world" -> start of "world"
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?,
s.pages.pin(.{ .screen = .{ .x = 8, .y = 0 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .word_left, boundary);
try testing.expectEqual(point.Point{ .screen = .{
.x = 6,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// word_left from start of "world" -> start of "hello"
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 10, .y = 0 } }).?,
s.pages.pin(.{ .screen = .{ .x = 6, .y = 0 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .word_left, boundary);
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// word_left from start of "hello" -> stays put (no previous word)
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 5, .y = 0 } }).?,
s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .word_left, boundary);
// Should stay at x=0 since there's no previous word
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: adjust word_left with multiple spaces" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, .{ .cols = 20, .rows = 10, .max_scrollback = 0 });
defer s.deinit();
try s.testWriteString("hello world");
const boundary = &[_]u21{ ' ', '\t' };
// word_left from middle of "world" -> start of "world" (skips within word)
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 12, .y = 0 } }).?,
s.pages.pin(.{ .screen = .{ .x = 10, .y = 0 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .word_left, boundary);
try testing.expectEqual(point.Point{ .screen = .{
.x = 8,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
// word_left from start of "world" -> start of "hello" (skips multiple spaces)
{
var sel = Selection.init(
s.pages.pin(.{ .screen = .{ .x = 12, .y = 0 } }).?,
s.pages.pin(.{ .screen = .{ .x = 8, .y = 0 } }).?,
false,
);
defer sel.deinit(&s);
sel.adjust(&s, .word_left, boundary);
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
}
test "Selection: order, standard" {
const testing = std.testing;
const alloc = testing.allocator;