From f6d1f27337bedb17d73e75a06cb3194a5686df38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Sun, 9 Nov 2025 18:59:38 -0500 Subject: [PATCH] feat: [hx] mm match brackets support --- src/keybind/builtin/helix.json | 2 +- src/tui/editor.zig | 3 +-- src/tui/mode/helix.zig | 45 ++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/keybind/builtin/helix.json b/src/keybind/builtin/helix.json index f016a75..1e9e063 100644 --- a/src/keybind/builtin/helix.json +++ b/src/keybind/builtin/helix.json @@ -463,7 +463,7 @@ ["x", "extend_line_below"], - ["m m", "match_brackets"], + ["m m", "match_brackets", "helix_sel_mode"], ["m s", "surround_add"], ["m r", "surround_replace"], ["m d", "surround_delete"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index e138bd3..92c734a 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -3498,7 +3498,7 @@ pub const Editor = struct { } pub const move_or_select_to_char_left_meta: Meta = .{ .arguments = &.{.integer} }; - fn match_bracket(root: Buffer.Root, original_cursor: Cursor, metrics: Buffer.Metrics) error{Stop}!struct { usize, usize } { + pub fn match_bracket(root: Buffer.Root, original_cursor: Cursor, metrics: Buffer.Metrics) error{Stop}!struct { usize, usize } { // Find match bracket fallback when tree-sitter is not available // Operates exclusively when opening and closing brackets are distinct, when no match is found returns error.Stop // on success row, col. @@ -3561,7 +3561,6 @@ pub const Editor = struct { try self.with_cursels_const(root, &move_to_match_bracket, self.metrics); self.clamp(); } - pub const goto_bracket_meta: Meta = .{ .description = "goto matching bracket" }; pub fn move_or_select_to_char_right(self: *Self, ctx: Context) Result { diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 9ede857..bd3322a 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -209,6 +209,15 @@ const cmds_ = struct { } pub const split_selection_on_newline_meta: Meta = .{ .description = "Add cursor to each line in selection helix" }; + pub fn match_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_once_arg(root, &match_bracket, ctx); + ed.clamp(); + } + pub const match_brackets_meta: Meta = .{ .description = "Goto matching bracket" }; + pub fn extend_line_below(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; @@ -443,6 +452,42 @@ const cmds_ = struct { pub const replace_with_character_helix_meta: Meta = .{ .description = "Replace with character" }; }; +fn match_bracket(root: Buffer.Root, cursel: *CurSel, ctx: command.Context, metrics: Buffer.Metrics) error{Stop}!void { + var symbol: []const u8 = undefined; + const mode: enum { helix_sel_mode, helix_nor_mode } = if ((ctx.args.match(.{tp.extract(&symbol)}) catch false) and + std.mem.eql(u8, @tagName(.helix_sel_mode), symbol)) .helix_sel_mode else .helix_nor_mode; + + if (mode == .helix_sel_mode) { + const begin: Cursor = if (cursel.selection) |sel| sel.begin else cursel.*.cursor; + if (cursel.*.selection) |*sel| { + const row, const col = Editor.match_bracket(root, cursel.*.cursor, metrics) catch blk: { + // Selection in hx mode requires to move to the left to begin manipulation + try cursel.*.cursor.move_left(root, metrics); + break :blk try Editor.match_bracket(root, cursel.*.cursor, metrics); + }; + cursel.*.cursor.row = row; + cursel.*.cursor.col = col; + sel.end = cursel.*.cursor; + + //Then to include the whole selection, requires to extend to the right + if (sel.is_reversed()) { + try sel.begin.move_right(root, metrics); + } else { + try cursel.*.cursor.move_right(root, metrics); + try sel.end.move_right(root, metrics); + } + } else { + cursel.*.selection = Selection.from_cursor(&begin); + cursel.*.selection.?.end = cursel.*.cursor; + } + } else { + const row, const col = try Editor.match_bracket(root, cursel.*.cursor, metrics); + cursel.*.cursor.row = row; + cursel.*.cursor.col = col; + cursel.*.selection = null; + } +} + fn move_to_word(ctx: command.Context, move: Editor.cursor_operator_const) command.Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return;