From f288d24e133b6e4a71c454c5b83e0c2545581bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Mon, 13 Oct 2025 00:13:03 -0500 Subject: [PATCH 1/4] hx: feature parity on copy paste (P before, R replace, r after) --- .gitignore | 1 + src/keybind/builtin/helix.json | 38 +++--- src/tui/editor.zig | 13 ++- src/tui/mode/helix.zig | 208 ++++++++++++++++++++++++++------- 4 files changed, 197 insertions(+), 63 deletions(-) diff --git a/.gitignore b/.gitignore index 278aa08..33f0f46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.cache/ +src/.cache /zig-out/ /.zig-cache/ diff --git a/src/keybind/builtin/helix.json b/src/keybind/builtin/helix.json index 0eeb9b9..15e015d 100644 --- a/src/keybind/builtin/helix.json +++ b/src/keybind/builtin/helix.json @@ -25,7 +25,6 @@ ["ctrl+^", "open_previous_file"], ["alt+.", "repeat_last_motion"], - ["alt+`", "to_upper"], ["alt+d", "delete_backward"], ["alt+c", "change_backward_helix"], @@ -60,7 +59,6 @@ ["alt+|", "shell_pipe_to"], ["alt+!", "shell_append_output"], - ["~", "switch_case"], ["T", "till_prev_char"], ["F", "move_to_char", "move_to_char_left"], ["W", "move_next_long_word_start"], @@ -72,15 +70,21 @@ ["O", ["enter_mode", "insert"], ["smart_insert_line_before"]], ["C", "add_cursor_down"], + ["S", "split_selection"], ["X", "extend_to_line_bounds"], ["?", "rfind"], ["N", "goto_prev_match"], ["*", "search_selection"], - ["U", "redo"], - ["P", "paste"], - ["Q", "replay_macro"], + ["~", "switch_case"], + ["`", "to_lower"], + ["alt+`", "to_upper"], + + ["r", "replace"], + ["P", "paste_clipboard_before"], + ["R", "replace_selections_with_clipboard"], + ["p", "paste_after"], [">", "indent"], ["<", "unindent"], @@ -108,8 +112,6 @@ ["t", "find_till_char"], ["f", "move_to_char", "move_to_char_right"], - ["`", "to_lower"], - ["home", "move_begin"], ["end", "move_end"], ["kp_home", "move_begin"], @@ -121,7 +123,7 @@ ["v", "enter_mode", "select"], - ["g g", "goto_line_vim"], + ["g g", "move_buffer_begin"], ["g e", "move_buffer_end"], ["g f", "goto_file"], ["g h", "move_begin"], @@ -129,7 +131,7 @@ ["g s", "smart_move_begin"], ["g d", "goto_definition"], ["g y", "goto_type_definition"], - ["g r", "references"], + ["g r", "goto_reference"], ["g i", "goto_implementation"], ["g t", "goto_window_top"], ["g c", "goto_window_center"], @@ -189,13 +191,15 @@ ["/", "find"], ["n", "goto_next_match"], + ["u", "undo"], + ["U", "redo"], ["y", ["enable_selection"], ["copy_helix"], ["enter_mode", "normal"]], ["%", "select_all"], - ["p", "paste_after"], ["q", "record_macro"], + ["Q", "replay_macro"], ["=", "format_selections"], @@ -267,8 +271,6 @@ ["b", "select_word_left"], ["w", "select_word_right"], - ["g g", "select_buffer_begin"], - ["g e", "select_buffer_end"], ["ctrl+b", "select_page_up"], ["ctrl+f", "select_page_down"], @@ -364,11 +366,9 @@ ["N", "extend_search_next"], ["*", "extend_search_prev"], - ["U", "redo"], - - ["P", "paste"], - - ["Q", "replay_macro"], + ["P", "paste_clipboard_before"], + ["R", ["replace_selections_with_clipboard"], ["enter_mode", "normal"]], + ["p", "paste_after"], [">", "indent"], ["<", "unindent"], @@ -488,12 +488,14 @@ ["/", "find"], ["n", "goto_next_match"], + ["u", "undo"], + ["U", "redo"], ["y", ["copy_helix"], ["enter_mode", "normal"]], - ["p", "paste_after"], ["q", "record_macro"], + ["Q", "replay_macro"], ["=", "format_selections"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index c3f2f61..e7364fe 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -150,7 +150,7 @@ pub const CurSel = struct { } } - fn disable_selection_normal(self: *Self) void { + pub fn disable_selection_normal(self: *Self) void { self.selection = null; } @@ -1917,7 +1917,7 @@ pub const Editor = struct { return try move(root, &cursel.cursor, allocator); } - fn with_selection_const(root: Buffer.Root, move: cursor_operator_const, cursel: *CurSel, metrics: Buffer.Metrics) error{Stop}!void { + pub fn with_selection_const(root: Buffer.Root, move: cursor_operator_const, cursel: *CurSel, metrics: Buffer.Metrics) error{Stop}!void { const sel = try cursel.enable_selection(root, metrics); try move(root, &sel.end, metrics); cursel.cursor = sel.end; @@ -2252,6 +2252,15 @@ pub const Editor = struct { return false; } + pub fn move_cursor_carriage_return(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { + if (is_eol_right(root, cursor, metrics)) { + try move_cursor_right(root, cursor, metrics); + } else { + try move_cursor_down(root, cursor, metrics); + try move_cursor_begin(root, cursor, metrics); + } + } + pub fn move_cursor_left(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { try cursor.move_left(root, metrics); } diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 7915e95..33abc5a 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; const log = @import("log"); const tp = @import("thespian"); const location_history = @import("location_history"); @@ -12,6 +13,8 @@ const Buffer = @import("Buffer"); const Cursor = Buffer.Cursor; const Selection = Buffer.Selection; +const serial_separator = "\n\t\t\n"; + var commands: Commands = undefined; pub fn init() !void { @@ -388,49 +391,25 @@ const cmds_ = struct { pub const select_to_char_right_helix_meta: Meta = .{ .description = "Move to char right" }; pub fn copy_helix(_: *void, _: Ctx) Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = ed.buf_root() catch return; - - for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| - tui.clipboard_add_chunk(try Editor.copy_selection(root, sel, tui.clipboard_allocator(), ed.metrics)); + try copy_internal_helix(); } pub const copy_helix_meta: Meta = .{ .description = "Copy selection to clipboard (helix)" }; pub fn paste_after(_: *void, ctx: Ctx) Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - - var text_: []const u8 = undefined; - const clipboard: []const []const u8 = if (ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text_)})) - &[_][]const u8{text_} - else - tui.clipboard_get_history() orelse return; - - const b = try ed.buf_for_update(); - var root = b.root; - - var bytes: usize = 0; - var cursel_idx = ed.cursels.items.len - 1; - var idx = clipboard.len - 1; - while (true) { - const cursel_ = &ed.cursels.items[cursel_idx]; - if (cursel_.*) |*cursel| { - const text = clipboard[idx]; - root = try insert(ed, root, cursel, text, b.allocator); - idx = if (idx == 0) clipboard.len - 1 else idx - 1; - bytes += text.len; - } - if (cursel_idx == 0) break; - cursel_idx -= 1; - } - ed.logger.print("paste: {d} bytes", .{bytes}); - - try ed.update_buf(root); - ed.clamp(); - ed.need_render(); + try paste_helix(ctx, insert_after); } pub const paste_after_meta: Meta = .{ .description = "Paste from clipboard after selection" }; + + pub fn replace_selections_with_clipboard(_: *void, ctx: Ctx) Result { + try paste_helix(ctx, insert_replace_selection); + } + pub const replace_selections_with_clipboard_meta: Meta = .{ .description = "Replace selection from clipboard" }; + + pub fn paste_clipboard_before(_: *void, ctx: Ctx) Result { + try paste_helix(ctx, insert_before); + } + + pub const paste_clipboard_before_meta: Meta = .{ .description = "Paste from clipboard before selection" }; }; fn move_cursor_word_left_helix(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { @@ -464,14 +443,84 @@ fn move_cursor_word_right_end_helix(root: Buffer.Root, cursor: *Cursor, metrics: try cursor.move_right(root, metrics); } -fn insert(ed: *Editor, root: Buffer.Root, cursel: *CurSel, s: []const u8, allocator: std.mem.Allocator) !Buffer.Root { - var root_ = root; - const cursor = &cursel.cursor; - if (cursel.selection == null) cursor.move_right(root_, ed.metrics) catch {}; +fn insert_before(editor: *Editor, root: Buffer.Root, cursel: *CurSel, s: []const u8, allocator: Allocator) !Buffer.Root { + var root_: Buffer.Root = root; + const cursor: *Cursor = &cursel.cursor; + + cursel.check_selection(root, editor.metrics); + if (s[s.len - 1] == '\n') { + if (cursel.selection) |*sel_| { + sel_.*.normalize(); + cursor.move_to(root, sel_.*.begin.row, sel_.*.begin.col, editor.metrics) catch {}; + } else { + cursor.move_begin(); + } + } else { + if (cursel.selection) |*sel_| { + sel_.*.normalize(); + cursor.move_to(root, sel_.*.begin.row, sel_.*.begin.col, editor.metrics) catch {}; + } + } + cursel.disable_selection_normal(); const begin = cursel.cursor; - cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, ed.metrics); + + cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, editor.metrics); cursor.target = cursor.col; - ed.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); + editor.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); + cursel.selection = Selection{ .begin = begin, .end = cursor.* }; + return root_; +} + +fn insert_replace_selection(editor: *Editor, root: Buffer.Root, cursel: *CurSel, s: []const u8, allocator: Allocator) !Buffer.Root { + var root_: Buffer.Root = root; + cursel.check_selection(root, editor.metrics); + + if (cursel.selection) |_| { + root_ = try editor.delete_selection(root, cursel, allocator); + } else { + // Replace current character when no explicit selection + try Editor.with_selection_const(root, move_noop, cursel, editor.metrics); + root_ = try editor.delete_selection(root, cursel, allocator); + } + + const cursor = &cursel.cursor; + const begin = cursel.cursor; + + cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, editor.metrics); + cursor.target = cursor.col; + editor.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); + cursel.selection = Selection{ .begin = begin, .end = cursor.* }; + return root_; +} + +fn insert_after(editor: *Editor, root: Buffer.Root, cursel: *CurSel, s: []const u8, allocator: Allocator) !Buffer.Root { + var root_: Buffer.Root = root; + const cursor = &cursel.cursor; + cursel.check_selection(root, editor.metrics); + if (s[s.len - 1] == '\n') { + if (cursel.selection) |*sel_| { + sel_.*.normalize(); + if (sel_.*.end.row == sel_.*.begin.row or sel_.*.end.col != 0) { + cursel.disable_selection_normal(); + Editor.move_cursor_carriage_return(root, cursor, editor.metrics) catch {}; + } + } else { + cursel.disable_selection_normal(); + Editor.move_cursor_carriage_return(root, cursor, editor.metrics) catch {}; + } + } else { + if (cursel.selection) |*sel_| { + sel_.*.normalize(); + cursor.move_to(root, sel_.*.end.row, sel_.*.end.col, editor.metrics) catch {}; + } else { + cursor.move_right(root_, editor.metrics) catch {}; + } + cursel.disable_selection_normal(); + } + const begin = cursel.cursor; + cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, editor.metrics); + cursor.target = cursor.col; + editor.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); cursel.selection = Selection{ .begin = begin, .end = cursor.* }; return root_; } @@ -561,6 +610,76 @@ fn move_cursor_long_word_right_end(root: Buffer.Root, cursor: *Cursor, metrics: try cursor.move_right(root, metrics); } +const pasting_function = @TypeOf(insert_before); + +fn paste_helix(ctx: command.Context, do_paste: pasting_function) command.Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + var text: []const u8 = undefined; + + if (!(ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text)}))) { + if (tui.get_clipboard()) |text_| text = text_ else return; + } + + ed.logger.print("paste: {d} bytes", .{text.len}); + + const b = try ed.buf_for_update(); + var root = b.root; + + if (std.mem.indexOf(u8, text, serial_separator)) |_| { + // Chunks from clipboard are paired to selections + // If more selections than chunks in the clipboard, the exceding selections + // use the last chunk in the clipboard + var pos: usize = 0; + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + if (std.mem.indexOfPos(u8, text, pos, serial_separator)) |next| { + root = try do_paste(ed, root, cursel, text[pos..next], b.allocator); + pos = next + serial_separator.len; + } else { + root = try do_paste(ed, root, cursel, text[pos..], b.allocator); + } + }; + } else { + // The clipboard has only one chunk, which is pasted in all selections + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + root = try do_paste(ed, root, cursel, text, b.allocator); + }; + } + + try ed.update_buf(root); + ed.clamp(); + ed.need_render(); +} + +fn copy_internal_helix() command.Result { + const mv = tui.mainview() orelse return; + const editor = mv.get_active_editor() orelse return; + const root = editor.buf_root() catch return; + var first = true; + var text = std.ArrayListUnmanaged(u8).empty; + defer text.deinit(editor.allocator); + + for (editor.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + if (cursel.selection) |sel| { + const copy_text = try Editor.copy_selection(root, sel, editor.allocator, editor.metrics); + if (first) { + first = false; + } else { + try text.appendSlice(editor.allocator, serial_separator); + } + try text.appendSlice(editor.allocator, copy_text); + } + }; + if (text.items.len > 0) { + if (text.items.len > 100) { + editor.logger.print("copy:{f}...", .{std.ascii.hexEscape(text.items[0..100], .lower)}); + } else { + editor.logger.print("copy:{f}", .{std.ascii.hexEscape(text.items, .lower)}); + } + editor.set_clipboard_internal(try text.toOwnedSlice(editor.allocator)); + } +} + const private = @This(); // exports for unittests pub const test_internal = struct { @@ -569,4 +688,7 @@ pub const test_internal = struct { pub const move_cursor_long_word_right_end = private.move_cursor_long_word_right_end; pub const move_cursor_word_left_helix = private.move_cursor_word_left_helix; pub const move_cursor_word_right_end_helix = private.move_cursor_word_right_end_helix; + pub const insert_before = private.insert_before; + pub const insert_replace_selection = private.insert_replace_selection; + pub const insert_after = private.insert_after; }; From 7faea783f33cd9e4fb18778d10ed54d5b913d597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Mon, 13 Oct 2025 15:19:27 -0500 Subject: [PATCH 2/4] refactor: hx paste(before, replace, after) Code simplification and zig idiomatics to improve code readability. --- src/keybind/builtin/helix.json | 4 +- src/tui/editor.zig | 13 +---- src/tui/mode/helix.zig | 96 +++++++++++++++++++--------------- 3 files changed, 57 insertions(+), 56 deletions(-) diff --git a/src/keybind/builtin/helix.json b/src/keybind/builtin/helix.json index 15e015d..a33e6d0 100644 --- a/src/keybind/builtin/helix.json +++ b/src/keybind/builtin/helix.json @@ -131,7 +131,7 @@ ["g s", "smart_move_begin"], ["g d", "goto_definition"], ["g y", "goto_type_definition"], - ["g r", "goto_reference"], + ["g r", "references"], ["g i", "goto_implementation"], ["g t", "goto_window_top"], ["g c", "goto_window_center"], @@ -426,7 +426,7 @@ ["g s", "smart_move_begin"], ["g d", "goto_definition"], ["g y", "goto_type_definition"], - ["g r", "goto_reference"], + ["g r", "references"], ["g i", "goto_implementation"], ["g t", "goto_window_top"], ["g c", "goto_window_center"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index e7364fe..01dec2a 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -2229,7 +2229,7 @@ pub const Editor = struct { return false; } - fn is_eol_right(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + pub fn is_eol_right(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { const line_width = root.line_width(cursor.row, metrics) catch return true; if (cursor.col >= line_width) return true; @@ -2252,15 +2252,6 @@ pub const Editor = struct { return false; } - pub fn move_cursor_carriage_return(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { - if (is_eol_right(root, cursor, metrics)) { - try move_cursor_right(root, cursor, metrics); - } else { - try move_cursor_down(root, cursor, metrics); - try move_cursor_begin(root, cursor, metrics); - } - } - pub fn move_cursor_left(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { try cursor.move_left(root, metrics); } @@ -2317,7 +2308,7 @@ pub const Editor = struct { if (is_eol_vim(root, cursor, metrics)) try move_cursor_left_vim(root, cursor, metrics); } - fn move_cursor_down(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) !void { + pub fn move_cursor_down(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) !void { cursor.move_down(root, metrics) catch |e| switch (e) { error.Stop => cursor.move_end(root, metrics), }; diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 33abc5a..f4456bf 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -408,7 +408,6 @@ const cmds_ = struct { pub fn paste_clipboard_before(_: *void, ctx: Ctx) Result { try paste_helix(ctx, insert_before); } - pub const paste_clipboard_before_meta: Meta = .{ .description = "Paste from clipboard before selection" }; }; @@ -443,85 +442,75 @@ fn move_cursor_word_right_end_helix(root: Buffer.Root, cursor: *Cursor, metrics: try cursor.move_right(root, metrics); } -fn insert_before(editor: *Editor, root: Buffer.Root, cursel: *CurSel, s: []const u8, allocator: Allocator) !Buffer.Root { +fn insert_before(editor: *Editor, root: Buffer.Root, cursel: *CurSel, text: []const u8, allocator: Allocator) !Buffer.Root { var root_: Buffer.Root = root; const cursor: *Cursor = &cursel.cursor; cursel.check_selection(root, editor.metrics); - if (s[s.len - 1] == '\n') { - if (cursel.selection) |*sel_| { - sel_.*.normalize(); - cursor.move_to(root, sel_.*.begin.row, sel_.*.begin.col, editor.metrics) catch {}; - } else { + if (cursel.selection) |sel_| { + var sel = sel_; + sel.normalize(); + cursor.move_to(root, sel.begin.row, sel.begin.col, editor.metrics) catch {}; + + if (text[text.len - 1] == '\n') { cursor.move_begin(); } - } else { - if (cursel.selection) |*sel_| { - sel_.*.normalize(); - cursor.move_to(root, sel_.*.begin.row, sel_.*.begin.col, editor.metrics) catch {}; - } + } else if (text[text.len - 1] == '\n') { + cursor.move_begin(); } + cursel.disable_selection_normal(); const begin = cursel.cursor; - - cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, editor.metrics); + cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, text, allocator, editor.metrics); cursor.target = cursor.col; - editor.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); cursel.selection = Selection{ .begin = begin, .end = cursor.* }; + editor.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, text.len); return root_; } -fn insert_replace_selection(editor: *Editor, root: Buffer.Root, cursel: *CurSel, s: []const u8, allocator: Allocator) !Buffer.Root { +fn insert_replace_selection(editor: *Editor, root: Buffer.Root, cursel: *CurSel, text: []const u8, allocator: Allocator) !Buffer.Root { + // replaces the selection, if no selection, replaces the current + // character and sets the selection to the replacement text var root_: Buffer.Root = root; cursel.check_selection(root, editor.metrics); - if (cursel.selection) |_| { - root_ = try editor.delete_selection(root, cursel, allocator); - } else { - // Replace current character when no explicit selection - try Editor.with_selection_const(root, move_noop, cursel, editor.metrics); - root_ = try editor.delete_selection(root, cursel, allocator); + if (cursel.selection == null) { + // Select current character to replace it + Editor.with_selection_const(root, move_noop, cursel, editor.metrics) catch {}; } + root_ = editor.delete_selection(root, cursel, allocator) catch root; const cursor = &cursel.cursor; const begin = cursel.cursor; - - cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, editor.metrics); + cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, text, allocator, editor.metrics); cursor.target = cursor.col; - editor.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); cursel.selection = Selection{ .begin = begin, .end = cursor.* }; + editor.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, text.len); return root_; } -fn insert_after(editor: *Editor, root: Buffer.Root, cursel: *CurSel, s: []const u8, allocator: Allocator) !Buffer.Root { +fn insert_after(editor: *Editor, root: Buffer.Root, cursel: *CurSel, text: []const u8, allocator: Allocator) !Buffer.Root { var root_: Buffer.Root = root; const cursor = &cursel.cursor; cursel.check_selection(root, editor.metrics); - if (s[s.len - 1] == '\n') { - if (cursel.selection) |*sel_| { - sel_.*.normalize(); - if (sel_.*.end.row == sel_.*.begin.row or sel_.*.end.col != 0) { - cursel.disable_selection_normal(); - Editor.move_cursor_carriage_return(root, cursor, editor.metrics) catch {}; - } - } else { - cursel.disable_selection_normal(); - Editor.move_cursor_carriage_return(root, cursor, editor.metrics) catch {}; - } + if (text[text.len - 1] == '\n') { + move_cursor_carriage_return(root, cursel.*, cursor, editor.metrics) catch {}; } else { - if (cursel.selection) |*sel_| { - sel_.*.normalize(); - cursor.move_to(root, sel_.*.end.row, sel_.*.end.col, editor.metrics) catch {}; + if (cursel.selection) |sel_| { + var sel = sel_; + sel.normalize(); + cursor.move_to(root, sel.end.row, sel.end.col, editor.metrics) catch {}; } else { cursor.move_right(root_, editor.metrics) catch {}; } - cursel.disable_selection_normal(); } + + cursel.disable_selection_normal(); const begin = cursel.cursor; - cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, editor.metrics); + cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, text, allocator, editor.metrics); cursor.target = cursor.col; - editor.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); cursel.selection = Selection{ .begin = begin, .end = cursor.* }; + editor.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, text.len); return root_; } @@ -668,6 +657,7 @@ fn copy_internal_helix() command.Result { try text.appendSlice(editor.allocator, serial_separator); } try text.appendSlice(editor.allocator, copy_text); + editor.allocator.free(copy_text); } }; if (text.items.len > 0) { @@ -680,6 +670,26 @@ fn copy_internal_helix() command.Result { } } +fn move_cursor_carriage_return(root: Buffer.Root, cursel: CurSel, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { + if (is_cursel_from_extend_line_below(cursel)) { + //The cursor is already beginning next line + return; + } + if (!Editor.is_eol_right(root, cursor, metrics)) { + try Editor.move_cursor_end(root, cursor, metrics); + } + try Editor.move_cursor_right(root, cursor, metrics); +} + +fn is_cursel_from_extend_line_below(cursel: CurSel) bool { + if (cursel.selection) |sel_| { + var sel = sel_; + sel.normalize(); + return sel.end.row != sel.begin.row and sel.end.col == 0; + } + return false; +} + const private = @This(); // exports for unittests pub const test_internal = struct { From eb05939b8126fa30c96604830e18b83d4f7d07cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Mon, 13 Oct 2025 17:09:03 -0500 Subject: [PATCH 3/4] refactor: use flow clipboard in hx mode hx users will envy flow clipboard history :P, fortunately it's possible to use F4 to shift modes --- src/tui/mode/helix.zig | 86 +++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 56 deletions(-) diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index f4456bf..20d27e7 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -391,7 +391,16 @@ const cmds_ = struct { pub const select_to_char_right_helix_meta: Meta = .{ .description = "Move to char right" }; pub fn copy_helix(_: *void, _: Ctx) Result { - try copy_internal_helix(); + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + tui.clipboard_clear_all(); + + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| + tui.clipboard_add_chunk(try Editor.copy_selection(root, sel, tui.clipboard_allocator(), ed.metrics)); + + ed.logger.print("copy: {d} selections", .{ed.cursels.items.len}); } pub const copy_helix_meta: Meta = .{ .description = "Copy selection to clipboard (helix)" }; @@ -604,72 +613,37 @@ const pasting_function = @TypeOf(insert_before); fn paste_helix(ctx: command.Context, do_paste: pasting_function) command.Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; - var text: []const u8 = undefined; + var text_: []const u8 = undefined; - if (!(ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text)}))) { - if (tui.get_clipboard()) |text_| text = text_ else return; - } - - ed.logger.print("paste: {d} bytes", .{text.len}); + const clipboard: []const []const u8 = if (ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text_)})) + &[_][]const u8{text_} + else + tui.clipboard_get_history() orelse return; const b = try ed.buf_for_update(); var root = b.root; - if (std.mem.indexOf(u8, text, serial_separator)) |_| { - // Chunks from clipboard are paired to selections - // If more selections than chunks in the clipboard, the exceding selections - // use the last chunk in the clipboard - var pos: usize = 0; - for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - if (std.mem.indexOfPos(u8, text, pos, serial_separator)) |next| { - root = try do_paste(ed, root, cursel, text[pos..next], b.allocator); - pos = next + serial_separator.len; - } else { - root = try do_paste(ed, root, cursel, text[pos..], b.allocator); - } - }; - } else { - // The clipboard has only one chunk, which is pasted in all selections - for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - root = try do_paste(ed, root, cursel, text, b.allocator); - }; - } + // Chunks from clipboard are paired to selections + // If more selections than chunks in the clipboard, the exceding selections + // use the last chunk in the clipboard + + var bytes: usize = 0; + for (ed.cursels.items, 0..) |*cursel_, idx| if (cursel_.*) |*cursel| { + if (idx < clipboard.len) { + root = try do_paste(ed, root, cursel, clipboard[idx], b.allocator); + bytes += clipboard[idx].len; + } else { + bytes += clipboard[clipboard.len - 1].len; + root = try do_paste(ed, root, cursel, clipboard[clipboard.len - 1], b.allocator); + } + }; + ed.logger.print("paste: {d} bytes", .{bytes}); try ed.update_buf(root); ed.clamp(); ed.need_render(); } -fn copy_internal_helix() command.Result { - const mv = tui.mainview() orelse return; - const editor = mv.get_active_editor() orelse return; - const root = editor.buf_root() catch return; - var first = true; - var text = std.ArrayListUnmanaged(u8).empty; - defer text.deinit(editor.allocator); - - for (editor.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - if (cursel.selection) |sel| { - const copy_text = try Editor.copy_selection(root, sel, editor.allocator, editor.metrics); - if (first) { - first = false; - } else { - try text.appendSlice(editor.allocator, serial_separator); - } - try text.appendSlice(editor.allocator, copy_text); - editor.allocator.free(copy_text); - } - }; - if (text.items.len > 0) { - if (text.items.len > 100) { - editor.logger.print("copy:{f}...", .{std.ascii.hexEscape(text.items[0..100], .lower)}); - } else { - editor.logger.print("copy:{f}", .{std.ascii.hexEscape(text.items, .lower)}); - } - editor.set_clipboard_internal(try text.toOwnedSlice(editor.allocator)); - } -} - fn move_cursor_carriage_return(root: Buffer.Root, cursel: CurSel, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { if (is_cursel_from_extend_line_below(cursel)) { //The cursor is already beginning next line From b07cd859fee5802eef60b3bbec929157d32e273f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Mon, 13 Oct 2025 17:33:02 -0500 Subject: [PATCH 4/4] refactor: hx removing commented test code and unused constant --- src/tui/mode/helix.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 20d27e7..4cf8bce 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -13,8 +13,6 @@ const Buffer = @import("Buffer"); const Cursor = Buffer.Cursor; const Selection = Buffer.Selection; -const serial_separator = "\n\t\t\n"; - var commands: Commands = undefined; pub fn init() !void {