From ce7cc48a722fc5410ae653dab31b3a56216f9113 Mon Sep 17 00:00:00 2001 From: Paul Graydon Date: Sun, 8 Mar 2026 16:23:21 +0100 Subject: [PATCH] feat: [vim] Add bracket textobject actions --- src/keybind/builtin/vim.json | 52 +++++++ src/tui/editor.zig | 2 +- src/tui/mode/vim.zig | 274 +++++++++++++++++++++++++++++++++++ 3 files changed, 327 insertions(+), 1 deletion(-) diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index 587f148f..07d82099 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -82,7 +82,20 @@ ["\"_dd", "delete_line"], ["diw", "cut_inside_word"], + ["di(", "cut_inside_parentheses"], + ["di)", "cut_inside_parentheses"], + ["di[", "cut_inside_square_brackets"], + ["di]", "cut_inside_square_brackets"], + ["di{", "cut_inside_braces"], + ["di}", "cut_inside_braces"], + ["daw", "cut_around_word"], + ["da(", "cut_around_parentheses"], + ["da)", "cut_around_parentheses"], + ["da[", "cut_around_square_brackets"], + ["da]", "cut_around_square_brackets"], + ["da{", "cut_around_braces"], + ["da}", "cut_around_braces"], ["cc", ["enter_mode", "insert"], ["cut_internal_vim"]], ["C", ["enter_mode", "insert"], ["cut_to_end_vim"]], @@ -91,12 +104,38 @@ ["cb", ["enter_mode", "insert"], ["cut_word_left_vim"]], ["ciw", ["enter_mode", "insert"], ["cut_inside_word"]], + ["ci(", ["enter_mode", "insert"], ["cut_inside_parentheses"]], + ["ci)", ["enter_mode", "insert"], ["cut_inside_parentheses"]], + ["ci[", ["enter_mode", "insert"], ["cut_inside_square_brackets"]], + ["ci]", ["enter_mode", "insert"], ["cut_inside_square_brackets"]], + ["ci{", ["enter_mode", "insert"], ["cut_inside_braces"]], + ["ci}", ["enter_mode", "insert"], ["cut_inside_braces"]], + ["caw", ["enter_mode", "insert"], ["cut_around_word"]], + ["ca(", ["enter_mode", "insert"], ["cut_around_parentheses"]], + ["ca)", ["enter_mode", "insert"], ["cut_around_parentheses"]], + ["ca[", ["enter_mode", "insert"], ["cut_around_square_brackets"]], + ["ca]", ["enter_mode", "insert"], ["cut_around_square_brackets"]], + ["ca{", ["enter_mode", "insert"], ["cut_around_braces"]], + ["ca}", ["enter_mode", "insert"], ["cut_around_braces"]], ["yy", ["copy_line_internal_vim"], ["cancel"]], ["yiw", ["copy_inside_word"], ["cancel"]], + ["yi(", ["copy_inside_parentheses"], ["cancel"]], + ["yi)", ["copy_inside_parentheses"], ["cancel"]], + ["yi[", ["copy_inside_square_brackets"], ["cancel"]], + ["yi]", ["copy_inside_square_brackets"], ["cancel"]], + ["yi{", ["copy_inside_braces"], ["cancel"]], + ["yi}", ["copy_inside_braces"], ["cancel"]], + ["yaw", ["copy_around_word"], ["cancel"]], + ["ya(", ["copy_around_parentheses"], ["cancel"]], + ["ya)", ["copy_around_parentheses"], ["cancel"]], + ["ya[", ["copy_around_square_brackets"], ["cancel"]], + ["ya]", ["copy_around_square_brackets"], ["cancel"]], + ["ya{", ["copy_around_braces"], ["cancel"]], + ["ya}", ["copy_around_braces"], ["cancel"]], ["", "move_scroll_half_page_up_vim"], ["", "move_scroll_half_page_down_vim"], @@ -169,7 +208,20 @@ ["e", "select_word_right_end_vim"], ["iw", "select_inside_word"], + ["i(", "select_inside_parentheses"], + ["i)", "select_inside_parentheses"], + ["i[", "select_inside_square_brackets"], + ["i]", "select_inside_square_brackets"], + ["i{", "select_inside_braces"], + ["i}", "select_inside_braces"], + ["aw", "select_around_word"], + ["a(", "select_around_parentheses"], + ["a)", "select_around_parentheses"], + ["a[", "select_around_square_brackets"], + ["a]", "select_around_square_brackets"], + ["a{", "select_around_braces"], + ["a}", "select_around_braces"], ["^", "smart_move_begin"], ["$", "select_end"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index e65dd6d5..74a99168 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -41,7 +41,7 @@ const double_click_time_ms = 350; const syntax_full_reparse_time_limit = 0; // ms (0 = always use incremental) const syntax_full_reparse_error_threshold = 3; // number of tree-sitter errors that trigger a full reparse -const bracket_search_radius = if (builtin.mode == std.builtin.OptimizeMode.Debug) 8_192 else 65_536; +pub const bracket_search_radius = if (builtin.mode == std.builtin.OptimizeMode.Debug) 8_192 else 65_536; pub const max_matches = if (builtin.mode == std.builtin.OptimizeMode.Debug) 10_000 else 100_000; pub const max_match_lines = 15; diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index f7067e50..043ed35a 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -8,6 +8,7 @@ const Buffer = @import("Buffer"); const Cursor = Buffer.Cursor; const CurSel = @import("../editor.zig").CurSel; const Editor = @import("../editor.zig").Editor; +const bracket_search_radius = @import("../editor.zig").bracket_search_radius; var commands: Commands = undefined; @@ -165,6 +166,60 @@ const cmds_ = struct { } pub const select_around_word_meta: Meta = .{ .description = "Select around word" }; + pub fn select_inside_parentheses(_: *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_parentheses_textobject, ed.metrics); + } + pub const select_inside_parentheses_meta: Meta = .{ .description = "Select inside ()" }; + + pub fn select_around_parentheses(_: *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_parentheses_textobject, ed.metrics); + } + pub const select_around_parentheses_meta: Meta = .{ .description = "Select around ()" }; + + pub fn select_inside_square_brackets(_: *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_square_brackets_textobject, ed.metrics); + } + pub const select_inside_square_brackets_meta: Meta = .{ .description = "Select inside []" }; + + pub fn select_around_square_brackets(_: *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_square_brackets_textobject, ed.metrics); + } + pub const select_around_square_brackets_meta: Meta = .{ .description = "Select around []" }; + + pub fn select_inside_braces(_: *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_braces_textobject, ed.metrics); + } + pub const select_inside_braces_meta: Meta = .{ .description = "Select inside {}" }; + + pub fn select_around_braces(_: *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_braces_textobject, ed.metrics); + } + pub const select_around_braces_meta: Meta = .{ .description = "Select around {}" }; + pub fn cut_inside_word(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; @@ -185,6 +240,66 @@ const cmds_ = struct { } pub const cut_around_word_meta: Meta = .{ .description = "Cut around word" }; + pub fn cut_inside_parentheses(_: *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_parentheses_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_parentheses_meta: Meta = .{ .description = "Cut inside ()" }; + + pub fn cut_around_parentheses(_: *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_parentheses_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_parentheses_meta: Meta = .{ .description = "Cut around ()" }; + + pub fn cut_inside_square_brackets(_: *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_square_brackets_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_square_brackets_meta: Meta = .{ .description = "Cut inside []" }; + + pub fn cut_around_square_brackets(_: *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_square_brackets_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_square_brackets_meta: Meta = .{ .description = "Cut around []" }; + + pub fn cut_inside_braces(_: *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_braces_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_braces_meta: Meta = .{ .description = "Cut inside {}" }; + + pub fn cut_around_braces(_: *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_braces_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_braces_meta: Meta = .{ .description = "Cut around {}" }; + pub fn copy_inside_word(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; @@ -204,6 +319,66 @@ const cmds_ = struct { try ed.copy_internal_vim(ctx); } pub const copy_around_word_meta: Meta = .{ .description = "Copy around word" }; + + pub fn copy_inside_parentheses(_: *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_parentheses_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_parentheses_meta: Meta = .{ .description = "Copy inside ()" }; + + pub fn copy_around_parentheses(_: *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_parentheses_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_parentheses_meta: Meta = .{ .description = "Copy around ()" }; + + pub fn copy_inside_square_brackets(_: *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_square_brackets_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_square_brackets_meta: Meta = .{ .description = "Copy inside []" }; + + pub fn copy_around_square_brackets(_: *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_square_brackets_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_square_brackets_meta: Meta = .{ .description = "Copy around []" }; + + pub fn copy_inside_braces(_: *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_braces_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_braces_meta: Meta = .{ .description = "Copy inside {}" }; + + pub fn copy_around_braces(_: *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_braces_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_braces_meta: Meta = .{ .description = "Copy around {}" }; }; fn is_tab_or_space(c: []const u8) bool { @@ -267,3 +442,102 @@ fn select_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Me sel.end = next; cursel.*.cursor = next; } + +fn select_inside_parentheses_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "(", ")", .inside); +} + +fn select_around_parentheses_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "(", ")", .around); +} + +fn select_inside_square_brackets_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "[", "]", .inside); +} + +fn select_around_square_brackets_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "[", "]", .around); +} + +fn select_inside_braces_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "{", "}", .inside); +} + +fn select_around_braces_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "{", "}", .around); +} + +fn select_bracket_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, opening_char: []const u8, closing_char: []const u8, scope: enum { inside, around }) !void { + const current = cursel.cursor; + var prev = cursel.cursor; + var next = cursel.cursor; + + const bracket_egc, _, _ = root.egc_at(current.row, current.col, metrics) catch { + return error.Stop; + }; + if (std.mem.eql(u8, bracket_egc, opening_char)) { + const closing_row, const closing_col = try Editor.match_bracket(root, current, metrics); + + prev = current; + next.row = closing_row; + next.col = closing_col; + } else if (std.mem.eql(u8, bracket_egc, closing_char)) { + const opening_row, const opening_col = try Editor.match_bracket(root, current, metrics); + + prev.row = opening_row; + prev.col = opening_col; + next = current; + } else { + const opening_pos, const closing_pos = find_bracket_pair(root, cursel, metrics, .left, opening_char) catch try find_bracket_pair(root, cursel, metrics, .right, opening_char); + + prev.row = opening_pos[0]; + prev.col = opening_pos[1]; + next.row = closing_pos[0]; + next.col = closing_pos[1]; + } + + prev.move_right(root, metrics) catch {}; + + if (scope == .around) { + prev.move_left(root, metrics) catch {}; + next.move_right(root, metrics) catch {}; + } + + const sel = cursel.enable_selection(root, metrics); + sel.begin = prev; + sel.end = next; + cursel.*.cursor = next; +} + +fn find_bracket_pair(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, direction: enum { left, right }, char: []const u8) error{Stop}!struct { struct { usize, usize }, struct { usize, usize } } { + const start = cursel.cursor; + var moving_cursor = cursel.cursor; + + var i: usize = 0; + while (i < bracket_search_radius) : (i += 1) { + switch (direction) { + .left => try moving_cursor.move_left(root, metrics), + .right => try moving_cursor.move_right(root, metrics), + } + + const curr_egc, _, _ = root.egc_at(moving_cursor.row, moving_cursor.col, metrics) catch { + return error.Stop; + }; + if (std.mem.eql(u8, char, curr_egc)) { + const closing_row, const closing_col = try Editor.match_bracket(root, moving_cursor, metrics); + + switch (direction) { + .left => if (closing_row > start.row or (closing_row == start.row and closing_col > start.col)) { + return .{ .{ moving_cursor.row, moving_cursor.col }, .{ closing_row, closing_col } }; + } else { + continue; + }, + .right => { + return .{ .{ moving_cursor.row, moving_cursor.col }, .{ closing_row, closing_col } }; + }, + } + } + } + + return error.Stop; +}