feat: [vim] Add word textobject actions

This commit is contained in:
Paul Graydon 2026-02-14 23:09:57 +01:00 committed by CJ van den Berg
parent a8437d6139
commit ba840b72e0
3 changed files with 149 additions and 3 deletions

View file

@ -81,14 +81,23 @@
["dgg", "cut_buffer_begin"],
["\"_dd", "delete_line"],
["diw", "cut_inside_word"],
["daw", "cut_around_word"],
["cc", ["enter_mode", "insert"], ["cut_internal_vim"]],
["C", ["enter_mode", "insert"], ["cut_to_end_vim"]],
["D", "cut_to_end_vim"],
["cw", ["enter_mode", "insert"], ["cut_word_right_vim"]],
["cb", ["enter_mode", "insert"], ["cut_word_left_vim"]],
["ciw", ["enter_mode", "insert"], ["cut_inside_word"]],
["caw", ["enter_mode", "insert"], ["cut_around_word"]],
["yy", ["copy_line_internal_vim"], ["cancel"]],
["yiw", ["copy_inside_word"], ["cancel"]],
["yaw", ["copy_around_word"], ["cancel"]],
["<C-u>", "move_scroll_half_page_up_vim"],
["<C-d>", "move_scroll_half_page_down_vim"],
@ -159,6 +168,9 @@
["B", "select_word_left"],
["e", "select_word_right_end_vim"],
["iw", "select_inside_word"],
["aw", "select_around_word"],
["^", "smart_move_begin"],
["$", "select_end"],
[":", "open_command_palette"],

View file

@ -2628,7 +2628,15 @@ pub const Editor = struct {
return cursor.test_at(root, is_whitespace, metrics);
}
fn is_non_whitespace_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
pub fn is_whitespace_or_eol_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
return cursor.test_at(root, is_whitespace_or_eol, metrics);
}
pub fn is_non_whitespace_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
return !cursor.test_at(root, is_whitespace, metrics);
}
pub fn is_non_whitespace_or_eol_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
return !cursor.test_at(root, is_whitespace_or_eol, metrics);
}
@ -3745,7 +3753,7 @@ pub const Editor = struct {
}
pub fn move_cursor_right_until_non_whitespace(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
move_cursor_right_until(root, cursor, is_non_whitespace_at_cursor, metrics);
move_cursor_right_until(root, cursor, is_non_whitespace_or_eol_at_cursor, metrics);
}
pub fn move_word_left(self: *Self, ctx: Context) Result {

View file

@ -2,6 +2,13 @@ const std = @import("std");
const command = @import("command");
const cmd = command.executeName;
const tui = @import("../tui.zig");
const Buffer = @import("Buffer");
const Cursor = Buffer.Cursor;
const CurSel = @import("../editor.zig").CurSel;
const Editor = @import("../editor.zig").Editor;
var commands: Commands = undefined;
pub fn init() !void {
@ -138,6 +145,125 @@ const cmds_ = struct {
//TODO
return undefined;
}
pub const copy_line_meta: Meta = .{ .description = "Copies the current line" };
pub fn select_inside_word(_: *void, _: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics);
}
pub const select_inside_word_meta: Meta = .{ .description = "Select inside word" };
pub fn select_around_word(_: *void, _: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics);
}
pub const select_around_word_meta: Meta = .{ .description = "Select around word" };
pub fn cut_inside_word(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_inside_word_meta: Meta = .{ .description = "Cut inside word" };
pub fn cut_around_word(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_around_word_meta: Meta = .{ .description = "Cut around word" };
pub fn copy_inside_word(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_inside_word_meta: Meta = .{ .description = "Copy inside word" };
pub fn copy_around_word(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_around_word_meta: Meta = .{ .description = "Copy around word" };
};
fn is_tab_or_space(c: []const u8) bool {
return (c[0] == ' ') or (c[0] == '\t');
}
fn is_tab_or_space_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
return cursor.test_at(root, is_tab_or_space, metrics);
}
fn is_not_tab_or_space_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
return !cursor.test_at(root, is_tab_or_space, metrics);
}
fn select_inside_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_word_textobject(root, cursel, metrics, .inside);
}
fn select_around_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_word_textobject(root, cursel, metrics, .around);
}
fn select_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, scope: enum { inside, around }) !void {
var prev = cursel.cursor;
var next = cursel.cursor;
if (cursel.cursor.test_at(root, Editor.is_non_word_char, metrics)) {
if (cursel.cursor.test_at(root, Editor.is_whitespace_or_eol, metrics)) {
Editor.move_cursor_left_until(root, &prev, Editor.is_non_whitespace_at_cursor, metrics);
Editor.move_cursor_right_until(root, &next, Editor.is_non_whitespace_at_cursor, metrics);
} else {
Editor.move_cursor_left_until(root, &prev, Editor.is_whitespace_or_eol_at_cursor, metrics);
Editor.move_cursor_right_until(root, &next, Editor.is_whitespace_or_eol_at_cursor, metrics);
}
prev.move_right(root, metrics) catch {};
} else {
Editor.move_cursor_left_until(root, &prev, Editor.is_word_boundary_left_vim, metrics);
Editor.move_cursor_right_until(root, &next, Editor.is_word_boundary_right_vim, metrics);
next.move_right(root, metrics) catch {};
}
if (scope == .around) {
const inside_prev = prev;
const inside_next = next;
if (next.test_at(root, is_tab_or_space, metrics)) {
Editor.move_cursor_right_until(root, &next, is_not_tab_or_space_at_cursor, metrics);
} else {
next = inside_next;
prev.move_left(root, metrics) catch {};
if (prev.test_at(root, is_tab_or_space, metrics)) {
Editor.move_cursor_left_until(root, &prev, is_not_tab_or_space_at_cursor, metrics);
prev.move_right(root, metrics) catch {};
} else {
prev = inside_prev;
}
}
}
const sel = cursel.enable_selection(root, metrics);
sel.begin = prev;
sel.end = next;
cursel.*.cursor = next;
}