diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index a844be61..587f148f 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -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"]], + ["", "move_scroll_half_page_up_vim"], ["", "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"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index bdac85ac..e65dd6d5 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -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 { diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index 38eacbee..f7067e50 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -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; +}