feat: goto_bracket goes to the pair bracket under cursor

Uses simple matching nesting, as a complement to tree-sitter approach.

Flow mode shorcut ctrl+shit+\ ala Code
This commit is contained in:
Igor Támara 2025-11-09 13:27:36 -05:00 committed by CJ van den Berg
parent 57547b80ff
commit 56ea0138a5
3 changed files with 78 additions and 0 deletions

View file

@ -50,6 +50,15 @@ pub const char_pairs = [_]struct { []const u8, []const u8 }{
.{ "«", "»" },
};
pub const open_close_pairs = [_]struct { []const u8, []const u8 }{
.{ "(", ")" },
.{ "[", "]" },
.{ "{", "}" },
.{ "", "" },
.{ "", "" },
.{ "«", "»" },
};
fn raw_byte_to_utf8(cp: u8, buf: []u8) ![]const u8 {
var utf16le: [1]u16 = undefined;
const utf16le_as_bytes = std.mem.sliceAsBytes(utf16le[0..]);

View file

@ -113,6 +113,7 @@
["ctrl+right", "move_word_right"],
["ctrl+kp_left", "move_word_left"],
["ctrl+kp_right", "move_word_right"],
["ctrl+shift+\\", "goto_bracket"],
["ctrl+backspace", "delete_word_left"],
["ctrl+delete", "delete_word_right"],
["alt+shift+f5", "toggle_inspector_view"],

View file

@ -40,6 +40,8 @@ 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 max_matches = if (builtin.mode == std.builtin.OptimizeMode.Debug) 10_000 else 100_000;
pub const max_match_lines = 15;
pub const max_match_batch = if (builtin.mode == std.builtin.OptimizeMode.Debug) 100 else 1000;
@ -3496,6 +3498,72 @@ 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 } {
// 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.
var cursor = original_cursor;
const bracket_egc, _, _ = root.egc_at(cursor.row, cursor.col, metrics) catch {
return error.Stop;
};
var was_pair = false;
var is_right_pair = false;
var moving_cursor: *Cursor = &cursor;
var move: enum { left, right } = .right;
var current = bracket_egc;
var closing = bracket_egc;
for (Buffer.unicode.open_close_pairs) |pair| if (std.mem.eql(u8, bracket_egc, pair[0]) or std.mem.eql(u8, bracket_egc, pair[1])) {
was_pair = true;
is_right_pair = std.mem.eql(u8, bracket_egc, pair[1]);
if (is_right_pair) {
current = pair[1];
closing = pair[0];
move = .left;
} else {
current = pair[0];
closing = pair[1];
}
break;
};
if (!was_pair) {
return error.Stop;
}
var i: usize = 0;
var balanced: u8 = 0;
while (i < bracket_search_radius) : (i += 1) {
switch (move) {
.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, current, curr_egc)) {
balanced += 1;
} else if (std.mem.eql(u8, closing, curr_egc)) {
if (balanced == 0) {
return .{ moving_cursor.row, moving_cursor.col };
}
balanced = balanced - 1;
}
}
return error.Stop;
}
fn move_to_match_bracket(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) error{Stop}!void {
const row, const col = try match_bracket(root, cursel.*.cursor, metrics);
cursel.*.cursor.row = row;
cursel.*.cursor.col = col;
}
pub fn goto_bracket(self: *Self, _: Context) Result {
const root = try self.buf_root();
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 {
const selected = if (self.get_primary().selection) |_| true else false;
if (selected) try self.select_to_char_right(ctx) else try self.move_to_char_right(ctx);