From 1e7d595309b54e37a8eefa6f6426ff691ef22524 Mon Sep 17 00:00:00 2001 From: UnsaltedScholar Date: Sun, 12 Apr 2026 17:56:19 -0400 Subject: [PATCH 1/2] Add angle bracket textobject actions --- src/buffer/unicode.zig | 2 ++ src/keybind/builtin/vim.json | 16 +++++++++ src/tui/mode/vim.zig | 66 ++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/src/buffer/unicode.zig b/src/buffer/unicode.zig index fc78be54..5a8d24d7 100644 --- a/src/buffer/unicode.zig +++ b/src/buffer/unicode.zig @@ -44,6 +44,7 @@ pub const char_pairs = [_]struct { []const u8, []const u8 }{ .{ "`", "`" }, .{ "(", ")" }, .{ "[", "]" }, + .{ "<", ">" }, .{ "{", "}" }, .{ "‘", "’" }, .{ "“", "”" }, @@ -56,6 +57,7 @@ pub const char_pairs = [_]struct { []const u8, []const u8 }{ pub const open_close_pairs = [_]struct { []const u8, []const u8 }{ .{ "(", ")" }, .{ "[", "]" }, + .{ "<", ">" }, .{ "{", "}" }, .{ "‘", "’" }, .{ "“", "”" }, diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index 8b3d9106..9849c546 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -86,6 +86,8 @@ ["di)", "cut_inside_parentheses"], ["di[", "cut_inside_square_brackets"], ["di]", "cut_inside_square_brackets"], + ["di", "cut_inside_angle_brackets"], + ["di", "cut_inside_angle_brackets"], ["di{", "cut_inside_braces"], ["di}", "cut_inside_braces"], ["di'", "cut_inside_single_quotes"], @@ -96,6 +98,8 @@ ["da)", "cut_around_parentheses"], ["da[", "cut_around_square_brackets"], ["da]", "cut_around_square_brackets"], + ["da", "cut_around_angle_brackets"], + ["da", "cut_around_angle_brackets"], ["da{", "cut_around_braces"], ["da}", "cut_around_braces"], ["da'", "cut_around_single_quotes"], @@ -112,6 +116,8 @@ ["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_angle_brackets"]], + ["ci", ["enter_mode", "insert"], ["cut_inside_angle_brackets"]], ["ci{", ["enter_mode", "insert"], ["cut_inside_braces"]], ["ci}", ["enter_mode", "insert"], ["cut_inside_braces"]], ["ci'", ["enter_mode", "insert"], ["cut_inside_single_quotes"]], @@ -122,6 +128,8 @@ ["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_angle_brackets"]], + ["ca", ["enter_mode", "insert"], ["cut_around_angle_brackets"]], ["ca{", ["enter_mode", "insert"], ["cut_around_braces"]], ["ca}", ["enter_mode", "insert"], ["cut_around_braces"]], ["ca'", ["enter_mode", "insert"], ["cut_around_single_quotes"]], @@ -134,6 +142,8 @@ ["yi)", ["copy_inside_parentheses"], ["cancel"]], ["yi[", ["copy_inside_square_brackets"], ["cancel"]], ["yi]", ["copy_inside_square_brackets"], ["cancel"]], + ["yi", ["copy_inside_angle_brackets"], ["cancel"]], + ["yi", ["copy_inside_angle_brackets"], ["cancel"]], ["yi{", ["copy_inside_braces"], ["cancel"]], ["yi}", ["copy_inside_braces"], ["cancel"]], ["yi'", ["copy_inside_single_quotes"], ["cancel"]], @@ -144,6 +154,8 @@ ["ya)", ["copy_around_parentheses"], ["cancel"]], ["ya[", ["copy_around_square_brackets"], ["cancel"]], ["ya]", ["copy_around_square_brackets"], ["cancel"]], + ["ya", ["copy_around_angle_brackets"], ["cancel"]], + ["ya", ["copy_around_angle_brackets"], ["cancel"]], ["ya{", ["copy_around_braces"], ["cancel"]], ["ya}", ["copy_around_braces"], ["cancel"]], ["ya'", ["copy_around_single_quotes"], ["cancel"]], @@ -224,6 +236,8 @@ ["i)", "select_inside_parentheses"], ["i[", "select_inside_square_brackets"], ["i]", "select_inside_square_brackets"], + ["i", "select_inside_angle_brackets"], + ["i", "select_inside_angle_brackets"], ["i{", "select_inside_braces"], ["i}", "select_inside_braces"], ["i'", "select_inside_single_quotes"], @@ -234,6 +248,8 @@ ["a)", "select_around_parentheses"], ["a[", "select_around_square_brackets"], ["a]", "select_around_square_brackets"], + ["a", "select_around_angle_brackets"], + ["a", "select_around_angle_brackets"], ["a{", "select_around_braces"], ["a}", "select_around_braces"], ["a'", "select_around_single_quotes"], diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index 823cc812..dc1646a4 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -202,6 +202,24 @@ const cmds_ = struct { } pub const select_around_square_brackets_meta: Meta = .{ .description = "Select around []" }; + pub fn select_inside_angle_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_angle_brackets_textobject, ed.metrics); + } + pub const select_inside_angle_brackets_meta: Meta = .{ .description = "Select inside <>" }; + + pub fn select_around_angle_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_angle_brackets_textobject, ed.metrics); + } + pub const select_around_angle_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; @@ -316,6 +334,26 @@ const cmds_ = struct { } pub const cut_around_square_brackets_meta: Meta = .{ .description = "Cut around []" }; + pub fn cut_inside_angle_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_angle_brackets_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_angle_brackets_meta: Meta = .{ .description = "Cut inside <>" }; + + pub fn cut_around_angle_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_angle_brackets_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_angle_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; @@ -436,6 +474,26 @@ const cmds_ = struct { } pub const copy_around_square_brackets_meta: Meta = .{ .description = "Copy around []" }; + pub fn copy_inside_angle_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_angle_brackets_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_angle_brackets_meta: Meta = .{ .description = "Copy inside <>" }; + + pub fn copy_around_angle_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_angle_brackets_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_angle_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; @@ -575,6 +633,14 @@ fn select_around_square_brackets_textobject(root: Buffer.Root, cursel: *CurSel, return try select_scope_textobject(root, cursel, metrics, "[", "]", .around); } +fn select_inside_angle_brackets_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_scope_textobject(root, cursel, metrics, "<", ">", .inside); +} + +fn select_around_angle_brackets_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + 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_scope_textobject(root, cursel, metrics, "{", "}", .inside); } From 66ed4a5af4d378de5138ad87a58ac4424070d577 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 14 Apr 2026 22:56:08 +0200 Subject: [PATCH 2/2] fix: pull the last line of a file up or down closes #527 --- src/tui/editor.zig | 56 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 347c13a5..69b45f5f 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -4232,11 +4232,25 @@ pub const Editor = struct { fn pull_cursel_up(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; const saved = cursel.*; + errdefer cursel.* = saved; const sel = cursel.expand_selection_to_line(root, self.metrics); var sfa = std.heap.stackFallback(4096, self.allocator); const sfa_allocator = sfa.get(); - const cut_text = copy_selection(root, sel.*, sfa_allocator, self.metrics) catch return error.Stop; - defer sfa_allocator.free(cut_text); + const cut_text_raw = copy_selection(root, sel.*, sfa_allocator, self.metrics) catch return error.Stop; + defer sfa_allocator.free(cut_text_raw); + + const is_last_no_nl = sel.end.row == cursel.cursor.row; + + var cut_text_buf: ?[]u8 = null; + defer if (cut_text_buf) |t| sfa_allocator.free(t); + const cut_text: []const u8 = if (is_last_no_nl) blk: { + const buf = sfa_allocator.alloc(u8, cut_text_raw.len + 1) catch return error.Stop; + cut_text_buf = buf; + @memcpy(buf[0..cut_text_raw.len], cut_text_raw); + buf[cut_text_raw.len] = '\n'; + break :blk buf; + } else cut_text_raw; + root = try self.delete_selection(root, cursel, allocator); try cursel.cursor.move_up(root, self.metrics); root = self.insert(root, cursel, cut_text, allocator) catch return error.Stop; @@ -4246,6 +4260,17 @@ pub const Editor = struct { try sel_.begin.move_up(root, self.metrics); try sel_.end.move_up(root, self.metrics); } + + if (is_last_no_nl) { + const last_content_row = root.lines() - 2; + var del_begin: Cursor = .{ .row = last_content_row, .col = 0 }; + del_begin.move_end(root, self.metrics); + var tmp: CurSel = .{ + .cursor = del_begin, + .selection = .{ .begin = del_begin, .end = .{ .row = last_content_row + 1, .col = 0 } }, + }; + root = try self.delete_selection(root, &tmp, allocator); + } return root; } @@ -4260,14 +4285,35 @@ pub const Editor = struct { fn pull_cursel_down(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; const saved = cursel.*; + errdefer cursel.* = saved; + const cursor_row_before = cursel.cursor.row; + const lines_before = root.lines(); const sel = cursel.expand_selection_to_line(root, self.metrics); var sfa = std.heap.stackFallback(4096, self.allocator); const sfa_allocator = sfa.get(); - const cut_text = copy_selection(root, sel.*, sfa_allocator, self.metrics) catch return error.Stop; + const cut_text = if (sel.empty()) + &.{} + else + copy_selection(root, sel.*, sfa_allocator, self.metrics) catch return error.Stop; defer sfa_allocator.free(cut_text); root = try self.delete_selection(root, cursel, allocator); - try cursel.cursor.move_down(root, self.metrics); - root = self.insert(root, cursel, cut_text, allocator) catch return error.Stop; + const moved_down = blk: { + cursel.cursor.move_down(root, self.metrics) catch break :blk false; + break :blk true; + }; + if (moved_down) { + root = self.insert(root, cursel, cut_text, allocator) catch return error.Stop; + } else { + if (cursor_row_before >= lines_before - 1) return error.Stop; + cursel.cursor.move_end(root, self.metrics); + root = self.insert(root, cursel, "\n", allocator) catch return error.Stop; + const cut_no_nl = if (std.mem.endsWith(u8, cut_text, "\n")) + cut_text[0 .. cut_text.len - 1] + else + cut_text; + if (cut_no_nl.len > 0) + root = self.insert(root, cursel, cut_no_nl, allocator) catch return error.Stop; + } cursel.* = saved; try cursel.cursor.move_down(root, self.metrics); if (cursel.selection) |*sel_| {