Add quote textobject actions

This commit is contained in:
UnsaltedScholar 2026-03-31 15:07:36 -04:00 committed by CJ van den Berg
parent 763935912f
commit 76cc8260bb
3 changed files with 314 additions and 24 deletions

View file

@ -88,6 +88,8 @@
["di]", "cut_inside_square_brackets"],
["di{", "cut_inside_braces"],
["di}", "cut_inside_braces"],
["di'", "cut_inside_single_quotes"],
["di\"", "cut_inside_double_quotes"],
["daw", "cut_around_word"],
["da(", "cut_around_parentheses"],
@ -96,6 +98,8 @@
["da]", "cut_around_square_brackets"],
["da{", "cut_around_braces"],
["da}", "cut_around_braces"],
["da'", "cut_around_single_quotes"],
["da\"", "cut_around_double_quotes"],
["cc", ["enter_mode", "insert"], ["cut_internal_vim"]],
["C", ["enter_mode", "insert"], ["cut_to_end_vim"]],
@ -110,6 +114,8 @@
["ci]", ["enter_mode", "insert"], ["cut_inside_square_brackets"]],
["ci{", ["enter_mode", "insert"], ["cut_inside_braces"]],
["ci}", ["enter_mode", "insert"], ["cut_inside_braces"]],
["ci'", ["enter_mode", "insert"], ["cut_inside_single_quotes"]],
["ci\"", ["enter_mode", "insert"], ["cut_inside_double_quotes"]],
["caw", ["enter_mode", "insert"], ["cut_around_word"]],
["ca(", ["enter_mode", "insert"], ["cut_around_parentheses"]],
@ -118,6 +124,8 @@
["ca]", ["enter_mode", "insert"], ["cut_around_square_brackets"]],
["ca{", ["enter_mode", "insert"], ["cut_around_braces"]],
["ca}", ["enter_mode", "insert"], ["cut_around_braces"]],
["ca'", ["enter_mode", "insert"], ["cut_around_single_quotes"]],
["ca\"", ["enter_mode", "insert"], ["cut_around_double_quotes"]],
["yy", ["copy_line_internal_vim"], ["cancel"]],
@ -128,6 +136,8 @@
["yi]", ["copy_inside_square_brackets"], ["cancel"]],
["yi{", ["copy_inside_braces"], ["cancel"]],
["yi}", ["copy_inside_braces"], ["cancel"]],
["yi'", ["copy_inside_single_quotes"], ["cancel"]],
["yi\"", ["copy_inside_double_quotes"], ["cancel"]],
["yaw", ["copy_around_word"], ["cancel"]],
["ya(", ["copy_around_parentheses"], ["cancel"]],
@ -136,6 +146,8 @@
["ya]", ["copy_around_square_brackets"], ["cancel"]],
["ya{", ["copy_around_braces"], ["cancel"]],
["ya}", ["copy_around_braces"], ["cancel"]],
["ya'", ["copy_around_single_quotes"], ["cancel"]],
["ya\"", ["copy_around_double_quotes"], ["cancel"]],
["<C-u>", "move_scroll_half_page_up_vim"],
["<C-d>", "move_scroll_half_page_down_vim"],
@ -214,6 +226,8 @@
["i]", "select_inside_square_brackets"],
["i{", "select_inside_braces"],
["i}", "select_inside_braces"],
["i'", "select_inside_single_quotes"],
["i\"", "select_inside_double_quotes"],
["aw", "select_around_word"],
["a(", "select_around_parentheses"],
@ -222,6 +236,8 @@
["a]", "select_around_square_brackets"],
["a{", "select_around_braces"],
["a}", "select_around_braces"],
["a'", "select_around_single_quotes"],
["a\"", "select_around_double_quotes"],
["^", "smart_move_begin"],
["$", "select_end"],

View file

@ -3974,6 +3974,126 @@ pub const Editor = struct {
}
pub const goto_bracket_meta: Meta = .{ .description = "Goto matching bracket" };
const QuoteRole = enum { opening, closing };
fn row_start_cursor(root: Buffer.Root, cursor: Cursor, metrics: Buffer.Metrics) Cursor {
var c = cursor;
while (true) {
var prev = c;
prev.move_left(root, metrics) catch break;
if (prev.row != c.row) break;
c = prev;
}
return c;
}
fn quote_is_escaped(root: Buffer.Root, quote_cursor: Cursor, metrics: Buffer.Metrics) bool {
var cursor = quote_cursor;
var backslashes: usize = 0;
while (true) {
var prev = cursor;
prev.move_left(root, metrics) catch break;
if (prev.row != cursor.row) break;
const egc, _, _ = root.egc_at(prev.row, prev.col, metrics) catch break;
if (!std.mem.eql(u8, egc, "\\")) break;
backslashes += 1;
cursor = prev;
}
return (backslashes % 2) == 1;
}
fn find_unescaped_quote(
root: Buffer.Root,
start: Cursor,
metrics: Buffer.Metrics,
direction: enum { left, right },
quote: []const u8,
) error{Stop}!Cursor {
var cursor = start;
var i: usize = 0;
while (i < bracket_search_radius) : (i += 1) {
switch (direction) {
.left => cursor.move_left(root, metrics) catch return error.Stop,
.right => cursor.move_right(root, metrics) catch return error.Stop,
}
const egc, _, _ = root.egc_at(cursor.row, cursor.col, metrics) catch {
return error.Stop;
};
if (!std.mem.eql(u8, egc, quote)) continue;
if (quote_is_escaped(root, cursor, metrics)) continue;
return cursor;
}
return error.Stop;
}
fn quote_role_on_row(
root: Buffer.Root,
quote_cursor: Cursor,
metrics: Buffer.Metrics,
quote: []const u8,
) error{Stop}!QuoteRole {
var cursor = row_start_cursor(root, .{ .row = quote_cursor.row, .col = 0 }, metrics);
var opening = true;
while (cursor.row == quote_cursor.row and cursor.col <= quote_cursor.col) {
const egc, _, _ = root.egc_at(cursor.row, cursor.col, metrics) catch {
return error.Stop;
};
if (std.mem.eql(u8, egc, quote) and !quote_is_escaped(root, cursor, metrics)) {
if (cursor.col == quote_cursor.col) {
return if (opening) .opening else .closing;
}
opening = !opening;
}
cursor.move_right(root, metrics) catch break;
}
return error.Stop;
}
pub fn find_quote_pair(
root: Buffer.Root,
original_cursor: Cursor,
metrics: Buffer.Metrics,
quote: []const u8,
) error{Stop}!struct { struct { usize, usize }, struct { usize, usize } } {
// Find nearest quote (prefer rightward)
const anchor =
find_unescaped_quote(root, original_cursor, metrics, .right, quote) catch find_unescaped_quote(root, original_cursor, metrics, .left, quote) catch return error.Stop;
const role = try quote_role_on_row(root, anchor, metrics, quote);
const other = switch (role) {
.opening => try find_unescaped_quote(root, anchor, metrics, .right, quote),
.closing => try find_unescaped_quote(root, anchor, metrics, .left, quote),
};
return switch (role) {
.opening => .{
.{ anchor.row, anchor.col },
.{ other.row, other.col },
},
.closing => .{
.{ other.row, other.col },
.{ anchor.row, anchor.col },
},
};
}
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);

View file

@ -220,6 +220,42 @@ const cmds_ = struct {
}
pub const select_around_braces_meta: Meta = .{ .description = "Select around {}" };
pub fn select_inside_single_quotes(_: *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_single_quotes_textobject, ed.metrics);
}
pub const select_inside_single_quotes_meta: Meta = .{ .description = "Select inside ''" };
pub fn select_around_single_quotes(_: *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_single_quotes_textobject, ed.metrics);
}
pub const select_around_single_quotes_meta: Meta = .{ .description = "Select around ''" };
pub fn select_inside_double_quotes(_: *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_double_quotes_textobject, ed.metrics);
}
pub const select_inside_double_quotes_meta: Meta = .{ .description = "Select inside \"\"" };
pub fn select_around_double_quotes(_: *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_double_quotes_textobject, ed.metrics);
}
pub const select_around_double_quotes_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;
@ -300,6 +336,46 @@ const cmds_ = struct {
}
pub const cut_around_braces_meta: Meta = .{ .description = "Cut around {}" };
pub fn cut_inside_single_quotes(_: *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_single_quotes_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_inside_single_quotes_meta: Meta = .{ .description = "Cut inside ''" };
pub fn cut_around_single_quotes(_: *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_single_quotes_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_around_single_quotes_meta: Meta = .{ .description = "Cut around ''" };
pub fn cut_inside_double_quotes(_: *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_double_quotes_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_inside_double_quotes_meta: Meta = .{ .description = "Cut inside \"\"" };
pub fn cut_around_double_quotes(_: *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_double_quotes_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_around_double_quotes_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;
@ -379,6 +455,46 @@ const cmds_ = struct {
try ed.copy_internal_vim(ctx);
}
pub const copy_around_braces_meta: Meta = .{ .description = "Copy around {}" };
pub fn copy_inside_single_quotes(_: *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_single_quotes_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_inside_single_quotes_meta: Meta = .{ .description = "Copy inside ''" };
pub fn copy_around_single_quotes(_: *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_single_quotes_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_around_single_quotes_meta: Meta = .{ .description = "Copy around ''" };
pub fn copy_inside_double_quotes(_: *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_double_quotes_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_inside_double_quotes_meta: Meta = .{ .description = "Copy inside \"\"" };
pub fn copy_around_double_quotes(_: *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_double_quotes_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_around_double_quotes_meta: Meta = .{ .description = "Copy around \"\"" };
};
fn is_tab_or_space(c: []const u8) bool {
@ -444,56 +560,94 @@ fn select_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Me
}
fn select_inside_parentheses_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_bracket_textobject(root, cursel, metrics, "(", ")", .inside);
return try select_scope_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);
return try select_scope_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);
return try select_scope_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);
return try select_scope_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);
return try select_scope_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);
return try select_scope_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 {
fn select_inside_single_quotes_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_scope_textobject(root, cursel, metrics, "'", "'", .inside);
}
fn select_around_single_quotes_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_scope_textobject(root, cursel, metrics, "'", "'", .around);
}
fn select_inside_double_quotes_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_scope_textobject(root, cursel, metrics, "\"", "\"", .inside);
}
fn select_around_double_quotes_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_scope_textobject(root, cursel, metrics, "\"", "\"", .around);
}
fn select_scope_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);
if (std.mem.eql(u8, opening_char, closing_char)) {
const opening_pos, const closing_pos =
try Editor.find_quote_pair(root, current, metrics, opening_char);
prev.row = opening_pos[0];
prev.col = opening_pos[1];
next.row = closing_pos[0];
next.col = closing_pos[1];
} else {
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 pair = find_bracket_pair(root, cursel, metrics, .left, opening_char) catch blk: {
break :blk try find_bracket_pair(root, cursel, metrics, .right, opening_char);
};
prev.row = pair[0][0];
prev.col = pair[0][1];
next.row = pair[1][0];
next.col = pair[1][1];
}
}
prev.move_right(root, metrics) catch {};