diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index b9ba0be..0abb834 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -37,7 +37,6 @@ ["ctrl+page_down", "next_tab"], ["ctrl+page_up", "previous_tab"], ["ctrl+shift+e", "switch_buffers"], - ["alt+shift+v", "clipboard_history"], ["ctrl+0", "reset_fontsize"], ["ctrl+plus", "adjust_fontsize", 1.0], ["ctrl+minus", "adjust_fontsize", -1.0], @@ -148,6 +147,7 @@ ["alt+shift+d", "dupe_up"], ["alt+shift+f", "format"], ["alt+shift+s", "filter", "sort", "-u"], + ["alt+shift+v", "paste"], ["alt+shift+i", "add_cursors_to_line_ends"], ["alt+shift+left", "expand_selection"], ["alt+shift+right", "shrink_selection"], @@ -450,6 +450,7 @@ ["ctrl+space", "mini_mode_cancel"], ["ctrl+backspace", "mini_mode_reset"], ["alt+v", "system_paste"], + ["alt+shift+v", "system_paste"], ["tab", "mini_mode_try_complete_file"], ["escape", "mini_mode_cancel"], ["enter", "mini_mode_select"], @@ -468,6 +469,7 @@ ["ctrl+space", "mini_mode_cancel"], ["ctrl+backspace", "mini_mode_delete_to_previous_path_segment"], ["alt+v", "system_paste"], + ["alt+shift+v", "system_paste"], ["shift+tab", "mini_mode_reverse_complete_file"], ["up", "mini_mode_reverse_complete_file"], ["down", "mini_mode_try_complete_file"], @@ -498,6 +500,7 @@ ["ctrl+space", "exit_mini_mode"], ["ctrl+enter", "mini_mode_insert_bytes", "\n"], ["ctrl+backspace", "mini_mode_reset"], + ["alt+shift+v", "system_paste"], ["alt+v", "system_paste"], ["alt+n", "goto_next_file"], ["alt+p", "goto_prev_file"], @@ -531,6 +534,7 @@ ["ctrl+space", "mini_mode_cancel"], ["ctrl+enter", "mini_mode_insert_bytes", "\n"], ["ctrl+backspace", "mini_mode_reset"], + ["alt+shift+v", "system_paste"], ["alt+v", "system_paste"], ["alt+n", "goto_next_match"], ["alt+p", "goto_prev_match"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index c3f2f61..9efdd02 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -2580,6 +2580,20 @@ pub const Editor = struct { } pub const scroll_view_bottom_meta: Meta = .{}; + fn set_clipboard(self: *Self, text: []const u8) void { + tui.set_clipboard(text); + if (builtin.os.tag == .windows) { + @import("renderer").copy_to_windows_clipboard(text) catch |e| + self.logger.print_err("clipboard", "failed to set clipboard: {any}", .{e}); + } else { + tui.rdr().copy_to_system_clipboard(text); + } + } + + pub fn set_clipboard_internal(_: *Self, text: []const u8) void { + tui.set_clipboard(text); + } + pub fn copy_selection(root: Buffer.Root, sel: Selection, text_allocator: Allocator, metrics: Buffer.Metrics) ![]u8 { var size: usize = 0; _ = try root.get_range(sel, null, &size, null, metrics); @@ -2638,43 +2652,72 @@ pub const Editor = struct { return root_; } - pub fn cut_to(self: *Self, move: cursor_operator_const, root_: Buffer.Root) !Buffer.Root { + pub fn cut_to(self: *Self, move: cursor_operator_const, root_: Buffer.Root, text_allocator: Allocator) !struct { []const u8, Buffer.Root } { var all_stop = true; var root = root_; + var text = std.ArrayListUnmanaged(u8).empty; + defer text.deinit(text_allocator); + var first = true; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { if (cursel.selection) |_| { - const cut_text, root = self.cut_selection(root, cursel, tui.clipboard_allocator()) catch continue; - tui.clipboard_add_chunk(cut_text); + const cut_text, root = self.cut_selection(root, cursel, text_allocator) catch continue; + defer text_allocator.free(cut_text); all_stop = false; + if (first) { + first = false; + } else { + try text.appendSlice(text_allocator, "\n"); + } + try text.appendSlice(text_allocator, cut_text); continue; } with_selection_const(root, move, cursel, self.metrics) catch continue; - const cut_text, root = self.cut_selection(root, cursel, tui.clipboard_allocator()) catch continue; - tui.clipboard_add_chunk(cut_text); + const cut_text, root = self.cut_selection(root, cursel, text_allocator) catch continue; + defer text_allocator.free(cut_text); + + if (first) { + first = false; + } else { + try text.appendSlice(text_allocator, "\n"); + } + try text.appendSlice(text_allocator, cut_text); all_stop = false; }; - return if (all_stop) error.Stop else root; + if (all_stop) + return error.Stop; + return .{ try text.toOwnedSlice(text_allocator), root }; } pub fn cut_internal_vim(self: *Self, _: Context) Result { const primary = self.get_primary(); const b = self.buf_for_update() catch return; var root = b.root; + var text = std.ArrayListUnmanaged(u8).empty; + defer text.deinit(self.allocator); if (self.cursels.items.len == 1) if (primary.selection) |_| {} else { + try text.appendSlice(self.allocator, "\n"); const sel = primary.enable_selection(root, self.metrics) catch return; try move_cursor_begin(root, &sel.begin, self.metrics); try move_cursor_end(root, &sel.end, self.metrics); try move_cursor_right(root, &sel.end, self.metrics); }; + var first = true; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - const cut_text, root = try self.cut_selection(root, cursel, tui.clipboard_allocator()); - tui.clipboard_add_chunk(cut_text); + const cut_text, root = try self.cut_selection(root, cursel, self.allocator); + defer self.allocator.free(cut_text); + if (first) { + first = false; + } else { + try text.appendSlice(self.allocator, "\n"); + } + try text.appendSlice(self.allocator, cut_text); }; try self.update_buf(root); + self.set_clipboard_internal(try text.toOwnedSlice(self.allocator)); self.clamp(); } pub const cut_internal_vim_meta: Meta = .{ .description = "Cut selection or current line to internal clipboard (vim)" }; @@ -2696,21 +2739,31 @@ pub const Editor = struct { else => return e, }; }; - var count: usize = 0; + var first = true; + var text = std.ArrayListUnmanaged(u8).empty; + defer text.deinit(self.allocator); for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - count += 1; - const cut_text, root = try self.cut_selection(root, cursel, tui.clipboard_allocator()); - tui.clipboard_add_chunk(cut_text); + const cut_text, root = try self.cut_selection(root, cursel, self.allocator); + defer self.allocator.free(cut_text); + if (first) { + first = false; + } else { + try text.appendSlice(self.allocator, "\n"); + } + try text.appendSlice(self.allocator, cut_text); }; try self.update_buf(root); + self.set_clipboard(try text.toOwnedSlice(self.allocator)); self.clamp(); - try tui.clipboard_send_to_system(count); } pub const cut_meta: Meta = .{ .description = "Cut selection or current line to clipboard" }; pub fn copy(self: *Self, _: Context) Result { const primary = self.get_primary(); const root = self.buf_root() catch return; + var first = true; + var text = std.ArrayListUnmanaged(u8).empty; + defer text.deinit(self.allocator); if (self.cursels.items.len == 1) if (primary.selection) |_| {} else { const sel = primary.enable_selection(root, self.metrics) catch return; @@ -2718,26 +2771,46 @@ pub const Editor = struct { try move_cursor_end(root, &sel.end, self.metrics); try move_cursor_right(root, &sel.end, self.metrics); }; - var count: usize = 0; - for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| { - count += 1; - tui.clipboard_add_chunk(try copy_selection(root, sel, tui.clipboard_allocator(), self.metrics)); + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + if (cursel.selection) |sel| { + const copy_text = try copy_selection(root, sel, self.allocator, self.metrics); + defer self.allocator.free(copy_text); + + if (first) { + first = false; + } else { + try text.appendSlice(self.allocator, "\n"); + } + try text.appendSlice(self.allocator, copy_text); + } }; - return tui.clipboard_send_to_system(count); + if (text.items.len > 0) { + if (text.items.len > 100) { + self.logger.print("copy:{f}...", .{std.ascii.hexEscape(text.items[0..100], .lower)}); + } else { + self.logger.print("copy:{f}", .{std.ascii.hexEscape(text.items, .lower)}); + } + self.set_clipboard(try text.toOwnedSlice(self.allocator)); + } } pub const copy_meta: Meta = .{ .description = "Copy selection to clipboard" }; - fn copy_cursel_file_name(self: *const Self) error{OutOfMemory}!usize { - tui.clipboard_add_chunk(try tui.clipboard_allocator().dupe(u8, self.file_path orelse "*")); - return 1; + fn copy_cursel_file_name( + self: *const Self, + writer: *std.Io.Writer, + ) Result { + if (self.file_path) |file_path| + try writer.writeAll(file_path) + else + try writer.writeByte('*'); } - fn copy_cursel_file_name_and_location(self: *const Self, cursel: *const CurSel) error{ WriteFailed, OutOfMemory }!void { - var buffer: std.Io.Writer.Allocating = .init(tui.clipboard_allocator()); - defer buffer.deinit(); - const writer = &buffer.writer; - - try writer.writeAll(self.file_path orelse "*"); + fn copy_cursel_file_name_and_location( + self: *const Self, + cursel: *const CurSel, + writer: *std.Io.Writer, + ) Result { + try self.copy_cursel_file_name(writer); if (cursel.selection) |sel_| { var sel = sel_; sel.normalize(); @@ -2758,27 +2831,34 @@ pub const Editor = struct { try writer.print(":{d}:{d}", .{ cursel.cursor.row + 1, cursel.cursor.col + 1 }) else try writer.print(":{d}", .{cursel.cursor.row + 1}); - - tui.clipboard_add_chunk(try buffer.toOwnedSlice()); - } - - fn copy_cursels_file_name_and_location(self: *const Self) error{OutOfMemory}!usize { - var count: usize = 0; - for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - count += 1; - self.copy_cursel_file_name_and_location(cursel) catch return error.OutOfMemory; - }; - return count; } pub fn copy_file_name(self: *Self, ctx: Context) Result { - var mode: enum { all, file_name_only } = .all; + var mode: enum { all, primary_only, file_name_only } = .all; _ = ctx.args.match(.{tp.extract(&mode)}) catch false; - const n = switch (mode) { - .file_name_only => try self.copy_cursel_file_name(), - .all => try self.copy_cursels_file_name_and_location(), - }; - return tui.clipboard_send_to_system(n); + var buffer: std.Io.Writer.Allocating = .init(self.allocator); + defer buffer.deinit(); + const writer = &buffer.writer; + var first = true; + switch (mode) { + .file_name_only => try self.copy_cursel_file_name(writer), + .primary_only => try self.copy_cursel_file_name_and_location( + self.get_primary(), + writer, + ), + else => for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + if (first) first = false else try writer.writeByte('\n'); + try self.copy_cursel_file_name_and_location(cursel, writer); + }, + } + const text = try buffer.toOwnedSlice(); + if (text.len > 0) { + if (text.len > 100) + self.logger.print("copy:{f}...", .{std.ascii.hexEscape(text[0..100], .lower)}) + else + self.logger.print("copy:{f}", .{std.ascii.hexEscape(text, .lower)}); + self.set_clipboard(text); + } } pub const copy_file_name_meta: Meta = .{ .description = "Copy file name and location to clipboard", @@ -2786,51 +2866,97 @@ pub const Editor = struct { pub fn copy_internal_vim(self: *Self, _: Context) Result { const root = self.buf_root() catch return; - for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| - tui.clipboard_add_chunk(try copy_selection(root, sel, tui.clipboard_allocator(), self.metrics)); + var first = true; + var text = std.ArrayListUnmanaged(u8).empty; + defer text.deinit(self.allocator); + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + if (cursel.selection) |sel| { + const copy_text = try copy_selection(root, sel, self.allocator, self.metrics); + defer self.allocator.free(copy_text); + if (first) { + first = false; + } else { + try text.appendSlice(self.allocator, "\n"); + } + try text.appendSlice(self.allocator, copy_text); + } + }; + if (text.items.len > 0) { + if (text.items.len > 100) { + self.logger.print("copy:{f}...", .{std.ascii.hexEscape(text.items[0..100], .lower)}); + } else { + self.logger.print("copy:{f}", .{std.ascii.hexEscape(text.items, .lower)}); + } + self.set_clipboard_internal(try text.toOwnedSlice(self.allocator)); + } } pub const copy_internal_vim_meta: Meta = .{ .description = "Copy selection to internal clipboard (vim)" }; pub fn copy_line_internal_vim(self: *Self, _: Context) Result { const primary = self.get_primary(); const root = self.buf_root() catch return; + var first = true; + var text = std.ArrayListUnmanaged(u8).empty; + defer text.deinit(self.allocator); + try text.appendSlice(self.allocator, "\n"); if (primary.selection) |_| {} else { const sel = primary.enable_selection(root, self.metrics) catch return; try move_cursor_begin(root, &sel.begin, self.metrics); try move_cursor_end(root, &sel.end, self.metrics); try move_cursor_right(root, &sel.end, self.metrics); } - for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| - tui.clipboard_add_chunk(try copy_selection(root, sel, tui.clipboard_allocator(), self.metrics)); + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + if (cursel.selection) |sel| { + const copy_text = try copy_selection(root, sel, self.allocator, self.metrics); + defer self.allocator.free(copy_text); + if (first) { + first = false; + } else { + try text.appendSlice(self.allocator, "\n"); + } + try text.appendSlice(self.allocator, copy_text); + } + }; + if (text.items.len > 0) { + if (text.items.len > 100) { + self.logger.print("copy:{f}...", .{std.ascii.hexEscape(text.items[0..100], .lower)}); + } else { + self.logger.print("copy:{f}", .{std.ascii.hexEscape(text.items, .lower)}); + } + self.set_clipboard_internal(try text.toOwnedSlice(self.allocator)); + } } pub const copy_line_internal_vim_meta: Meta = .{ .description = "Copy line to internal clipboard (vim)" }; pub fn paste(self: *Self, ctx: Context) Result { - 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; - + 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; + } + self.logger.print("paste: {d} bytes", .{text.len}); const b = try self.buf_for_update(); var root = b.root; - - var bytes: usize = 0; - var cursel_idx = self.cursels.items.len - 1; - var idx = clipboard.len - 1; - while (true) { - const cursel_ = &self.cursels.items[cursel_idx]; - if (cursel_.*) |*cursel| { - const text = clipboard[idx]; - root = try self.insert(root, cursel, text, b.allocator); - idx = if (idx == 0) clipboard.len - 1 else idx - 1; - bytes += text.len; + if (self.cursels.items.len == 1) { + const primary = self.get_primary(); + root = try self.insert(root, primary, text, b.allocator); + } else { + if (std.mem.indexOfScalar(u8, text, '\n')) |_| { + var pos: usize = 0; + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + if (std.mem.indexOfScalarPos(u8, text, pos, '\n')) |next| { + root = try self.insert(root, cursel, text[pos..next], b.allocator); + pos = next + 1; + } else { + root = try self.insert(root, cursel, text[pos..], b.allocator); + pos = 0; + } + }; + } else { + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + root = try self.insert(root, cursel, text, b.allocator); + }; } - if (cursel_idx == 0) break; - cursel_idx -= 1; } - self.logger.print("paste: {d} bytes", .{bytes}); - try self.update_buf(root); self.clamp(); self.need_render(); @@ -2838,30 +2964,43 @@ pub const Editor = struct { pub const paste_meta: Meta = .{ .description = "Paste from internal clipboard" }; pub fn paste_internal_vim(self: *Self, ctx: Context) Result { - 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; + 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; + } + self.logger.print("paste: {d} bytes", .{text.len}); const b = try self.buf_for_update(); var root = b.root; - var bytes: usize = 0; - var cursel_idx = self.cursels.items.len - 1; - var idx = clipboard.len - 1; - while (true) { - const cursel_ = &self.cursels.items[cursel_idx]; - if (cursel_.*) |*cursel| { - const text = clipboard[idx]; - root = try self.insert_line_vim(root, cursel, text, b.allocator); - idx = if (idx == 0) clipboard.len - 1 else idx - 1; - bytes += text.len; + if (std.mem.eql(u8, text[text.len - 1 ..], "\n")) text = text[0 .. text.len - 1]; + + if (std.mem.indexOfScalar(u8, text, '\n')) |idx| { + if (idx == 0) { + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + try move_cursor_end(root, &cursel.cursor, self.metrics); + root = try self.insert(root, cursel, "\n", b.allocator); + }; + text = text[1..]; + } + if (self.cursels.items.len == 1) { + const primary = self.get_primary(); + root = try self.insert_line_vim(root, primary, text, b.allocator); + } else { + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + root = try self.insert_line_vim(root, cursel, text, b.allocator); + }; + } + } else { + if (self.cursels.items.len == 1) { + const primary = self.get_primary(); + root = try self.insert(root, primary, text, b.allocator); + } else { + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + root = try self.insert(root, cursel, text, b.allocator); + }; } - if (cursel_idx == 0) break; - cursel_idx -= 1; } - self.logger.print("paste: {d} bytes", .{bytes}); try self.update_buf(root); self.clamp(); @@ -2879,7 +3018,8 @@ pub const Editor = struct { pub fn cut_forward_internal(self: *Self, _: Context) Result { const b = try self.buf_for_update(); - const root = try self.cut_to(move_cursor_right, b.root); + const text, const root = try self.cut_to(move_cursor_right, b.root, self.allocator); + self.set_clipboard_internal(text); try self.update_buf(root); self.clamp(); } @@ -2962,7 +3102,8 @@ pub const Editor = struct { pub fn cut_buffer_end(self: *Self, _: Context) Result { const b = try self.buf_for_update(); - const root = try self.cut_to(move_cursor_buffer_end, b.root); + const text, const root = try self.cut_to(move_cursor_buffer_end, b.root, self.allocator); + self.set_clipboard_internal(text); try self.update_buf(root); self.clamp(); } @@ -2970,7 +3111,8 @@ pub const Editor = struct { pub fn cut_buffer_begin(self: *Self, _: Context) Result { const b = try self.buf_for_update(); - const root = try self.cut_to(move_cursor_buffer_begin, b.root); + const text, const root = try self.cut_to(move_cursor_buffer_begin, b.root, self.allocator); + self.set_clipboard_internal(text); try self.update_buf(root); self.clamp(); } @@ -2978,7 +3120,8 @@ pub const Editor = struct { pub fn cut_word_left_vim(self: *Self, _: Context) Result { const b = try self.buf_for_update(); - const root = try self.cut_to(move_cursor_word_left_vim, b.root); + const text, const root = try self.cut_to(move_cursor_word_left_vim, b.root, self.allocator); + self.set_clipboard_internal(text); try self.update_buf(root); self.clamp(); } @@ -2994,7 +3137,8 @@ pub const Editor = struct { pub fn cut_word_right_vim(self: *Self, _: Context) Result { const b = try self.buf_for_update(); - const root = try self.cut_to(move_cursor_word_right_vim, b.root); + const text, const root = try self.cut_to(move_cursor_word_right_vim, b.root, self.allocator); + self.set_clipboard_internal(text); try self.update_buf(root); self.clamp(); } @@ -3018,7 +3162,8 @@ pub const Editor = struct { pub fn cut_to_end_vim(self: *Self, _: Context) Result { const b = try self.buf_for_update(); - const root = try self.cut_to(move_cursor_end_vim, b.root); + const text, const root = try self.cut_to(move_cursor_end_vim, b.root, self.allocator); + self.set_clipboard_internal(text); try self.update_buf(root); self.clamp(); } diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 9fe0104..2bb510e 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1371,13 +1371,6 @@ pub fn write_restore_info(self: *Self) void { cbor.writeValue(writer, null) catch return; } - if (tui.clipboard_get_history()) |clipboard| { - cbor.writeArrayHeader(writer, clipboard.len) catch return; - for (clipboard) |item| cbor.writeValue(writer, item) catch return; - } else { - cbor.writeValue(writer, null) catch return; - } - const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); buffer_manager.write_state(writer) catch return; @@ -1404,16 +1397,6 @@ fn read_restore_info(self: *Self) !void { tp.trace(tp.channel.debug, .{ "mainview", "extract" }); var editor_file_path: ?[]const u8 = undefined; if (!try cbor.matchValue(&iter, cbor.extract(&editor_file_path))) return error.Stop; - - tui.clipboard_clear_all(); - var len = try cbor.decodeArrayHeader(&iter); - const clipboard_allocator = tui.clipboard_allocator(); - while (len > 0) : (len -= 1) { - var chunk: []const u8 = undefined; - if (!try cbor.matchValue(&iter, cbor.extract(&chunk))) return error.Stop; - tui.clipboard_add_chunk(try clipboard_allocator.dupe(u8, chunk)); - } - try self.buffer_manager.extract_state(&iter); if (self.widgets.get("tabs")) |tabs_widget| diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 7915e95..8674d0b 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -306,7 +306,8 @@ const cmds_ = struct { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; const b = try ed.buf_for_update(); - const root = try ed.cut_to(move_noop, b.root); + const text, const root = try ed.cut_to(move_noop, b.root, ed.allocator); + ed.set_clipboard_internal(text); try ed.update_buf(root); ed.clamp(); } @@ -391,9 +392,33 @@ const cmds_ = struct { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; const root = ed.buf_root() catch return; + var first = true; + var buffer: std.Io.Writer.Allocating = .init(ed.allocator); + defer buffer.deinit(); + const writer = &buffer.writer; - 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)); + if (ed.get_primary().selection) |sel| if (sel.begin.col == 0 and sel.end.row > sel.begin.row) try writer.writeAll("\n"); + + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + if (cursel.selection) |sel| { + const copy_text = try Editor.copy_selection(root, sel, ed.allocator, ed.metrics); + if (first) { + first = false; + } else { + try writer.writeAll("\n"); + } + try writer.writeAll(copy_text); + } + }; + const text = buffer.toOwnedSlice() catch &.{}; + if (text.len > 0) { + if (text.len > 100) { + ed.logger.print("copy:{f}...", .{std.ascii.hexEscape(text[0..100], .lower)}); + } else { + ed.logger.print("copy:{f}", .{std.ascii.hexEscape(text, .lower)}); + } + ed.set_clipboard_internal(text); + } } pub const copy_helix_meta: Meta = .{ .description = "Copy selection to clipboard (helix)" }; @@ -401,30 +426,26 @@ const cmds_ = struct { 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; + 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; - 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]; + if (std.mem.eql(u8, text[text.len - 1 ..], "\n")) text = text[0 .. text.len - 1]; + + if (std.mem.indexOfScalar(u8, text, '\n') != null and text[0] == '\n') { + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + root = try insert_line(ed, root, cursel, text, b.allocator); + }; + } else { + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { 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(); @@ -476,6 +497,19 @@ fn insert(ed: *Editor, root: Buffer.Root, cursel: *CurSel, s: []const u8, alloca return root_; } +fn insert_line(ed: *Editor, root: Buffer.Root, cursel: *CurSel, s: []const u8, allocator: std.mem.Allocator) !Buffer.Root { + var root_ = root; + const cursor = &cursel.cursor; + cursel.disable_selection(root, ed.metrics); + cursel.cursor.move_end(root, ed.metrics); + var begin = cursel.cursor; + begin.move_right(root, ed.metrics) catch {}; + cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, ed.metrics); + cursor.target = cursor.col; + cursel.selection = Selection{ .begin = begin, .end = cursor.* }; + return root_; +} + fn is_not_whitespace_or_eol(c: []const u8) bool { return !Editor.is_whitespace_or_eol(c); } diff --git a/src/tui/mode/overlay/clipboard_palette.zig b/src/tui/mode/overlay/clipboard_palette.zig deleted file mode 100644 index c4b770e..0000000 --- a/src/tui/mode/overlay/clipboard_palette.zig +++ /dev/null @@ -1,100 +0,0 @@ -const std = @import("std"); -const cbor = @import("cbor"); -const tp = @import("thespian"); -const root = @import("soft_root").root; -const command = @import("command"); - -const tui = @import("../../tui.zig"); -pub const Type = @import("palette.zig").Create(@This()); -const module_name = @typeName(@This()); - -pub const label = "Clipboard history"; -pub const name = " clipboard"; -pub const description = "clipboard"; -pub const icon = " "; - -pub const Entry = struct { - label: []const u8, - idx: usize, -}; - -pub fn load_entries(palette: *Type) !usize { - const history = tui.clipboard_get_history() orelse &.{}; - - if (history.len > 0) { - var idx = history.len - 1; - while (true) : (idx -= 1) { - var label_ = history[idx]; - while (label_.len > 0) switch (label_[0]) { - ' ', '\t', '\n' => label_ = label_[1..], - else => break, - }; - (try palette.entries.addOne(palette.allocator)).* = .{ - .label = label_, - .idx = idx, - }; - if (idx == 0) break; - } - } - return if (palette.entries.items.len == 0) label.len + 3 else 10; -} - -pub fn clear_entries(palette: *Type) void { - palette.entries.clearRetainingCapacity(); -} - -pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { - var value: std.Io.Writer.Allocating = .init(palette.allocator); - defer value.deinit(); - const writer = &value.writer; - try cbor.writeValue(writer, entry.label); - - var hint: std.Io.Writer.Allocating = .init(palette.allocator); - defer hint.deinit(); - const item = if (tui.clipboard_get_history()) |clipboard| clipboard[entry.idx] else &.{}; - var line_count: usize = 1; - for (0..item.len) |i| if (item[i] == '\n') { - line_count += 1; - }; - if (line_count > 1) - try hint.writer.print(" {d} lines", .{line_count}) - else - try hint.writer.print(" {d} {s}", .{ item.len, if (item.len == 1) "byte " else "bytes" }); - try cbor.writeValue(writer, hint.written()); - - try cbor.writeValue(writer, matches orelse &[_]usize{}); - try cbor.writeValue(writer, entry.idx); - try palette.menu.add_item_with_handler(value.written(), select); - palette.items += 1; -} - -fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { - var unused: []const u8 = undefined; - var idx: usize = undefined; - var iter = button.opts.label; - if (!(cbor.matchString(&iter, &unused) catch false)) return; - if (!(cbor.matchString(&iter, &unused) catch false)) return; - var len = cbor.decodeArrayHeader(&iter) catch return; - while (len > 0) : (len -= 1) - cbor.skipValue(&iter) catch return; - if (!(cbor.matchValue(&iter, cbor.extract(&idx)) catch false)) return; - tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); - - const history = tui.clipboard_get_history() orelse return; - if (history.len <= idx) return; - tp.self_pid().send(.{ "cmd", "paste", .{history[idx]} }) catch {}; -} - -pub fn delete_item(menu: *Type.MenuType, button: *Type.ButtonType) bool { - var unused: []const u8 = undefined; - var idx: usize = undefined; - var iter = button.opts.label; - if (!(cbor.matchString(&iter, &unused) catch false)) return false; - if (!(cbor.matchString(&iter, &unused) catch false)) return false; - var len = cbor.decodeArrayHeader(&iter) catch return false; - while (len > 0) : (len -= 1) - cbor.skipValue(&iter) catch return false; - if (!(cbor.matchValue(&iter, cbor.extract(&idx)) catch false)) return false; - command.executeName("clipboard_delete", command.fmt(.{idx})) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - return true; //refresh list -} diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 18986ea..5ddf392 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -75,7 +75,7 @@ fontfaces_: std.ArrayListUnmanaged([]const u8) = .{}, enable_mouse_idle_timer: bool = false, query_cache_: *syntax.QueryCache, frames_rendered_: usize = 0, -clipboard: ?std.ArrayList([]const u8) = null, +clipboard: ?[]const u8 = null, color_scheme: enum { dark, light } = .dark, color_scheme_locked: bool = false, @@ -270,7 +270,7 @@ fn deinit(self: *Self) void { self.logger.deinit(); self.query_cache_.deinit(); root.free_config(self.allocator, self.config_bufs); - self.clipboard_deinit(); + if (self.clipboard) |text| self.allocator.free(text); self.allocator.destroy(self); } @@ -1275,21 +1275,6 @@ const cmds = struct { @import("mode/helix.zig").deinit(); } pub const exit_helix_mode_meta: Meta = .{}; - - pub fn clipboard_history(_: *Self, _: Ctx) Result { - return try open_overlay(@import("mode/overlay/clipboard_palette.zig").Type); - } - pub const clipboard_history_meta: Meta = .{ .description = "Paste from clipboard history" }; - - pub fn clipboard_delete(self: *Self, ctx: Ctx) Result { - var idx: usize = 0; - if (!try ctx.args.match(.{tp.extract(&idx)})) - return error.InvalidClipboardDeleteArgument; - const clipboard = if (self.clipboard) |*clipboard| clipboard else return; - const removed = clipboard.orderedRemove(idx); - self.allocator.free(removed); - } - pub const clipboard_delete_meta: Meta = .{}; }; pub const MiniMode = struct { @@ -1782,73 +1767,14 @@ fn widget_type_config_variable(widget_type: WidgetType) *ConfigWidgetStyle { }; } -fn clipboard_deinit(self: *Self) void { - if (self.clipboard) |*clipboard| { - for (clipboard.items) |chunk| - self.allocator.free(chunk); - clipboard.deinit(self.allocator); - } - self.clipboard = null; -} - -pub fn clipboard_allocator() Allocator { +pub fn get_clipboard() ?[]const u8 { const self = current(); - return self.allocator; + return self.clipboard; } -pub fn clipboard_get_history() ?[]const []const u8 { +pub fn set_clipboard(text: []const u8) void { const self = current(); - return if (self.clipboard) |clipboard| clipboard.items else null; -} - -pub fn clipboard_peek_chunk() ?[]const u8 { - const self = current(); - const clipboard = self.clipboard orelse return null; - return clipboard[clipboard.len - 1]; -} - -pub fn clipboard_clear_all() void { - const self = current(); - self.clipboard_deinit(); -} - -pub fn clipboard_add_chunk(text: []const u8) void { - const self = current(); - const clipboard = if (self.clipboard) |*clipboard| clipboard else blk: { - self.clipboard = .empty; - break :blk &self.clipboard.?; - }; - const chunk = clipboard.addOne(self.allocator) catch @panic("OOM clipboard_add_chunk"); - chunk.* = text; -} - -pub fn clipboard_send_to_system(n_chunks: usize) error{ Stop, WriteFailed }!void { - const self = current(); - var buffer: std.Io.Writer.Allocating = .init(self.allocator); - defer buffer.deinit(); - const writer = &buffer.writer; - const clipboard = if (self.clipboard) |clipboard| clipboard.items else return error.Stop; - if (clipboard.len < n_chunks) return error.Stop; - if (n_chunks == 1) return self.clipboard_send_to_system_internal(clipboard[clipboard.len - 1]); - var first = true; - const chunks = clipboard[clipboard.len - n_chunks ..]; - for (chunks) |chunk| { - if (first) first = false else try writer.writeByte('\n'); - try writer.writeAll(chunk); - } -} - -fn clipboard_send_to_system_internal(self: *Self, text: []const u8) void { - if (text.len > 0) { - if (text.len > 100) - self.logger.print("copy:{f}...", .{std.ascii.hexEscape(text[0..100], .lower)}) - else - self.logger.print("copy:{f}", .{std.ascii.hexEscape(text, .lower)}); - } - if (builtin.os.tag == .windows) { - @import("renderer").copy_to_windows_clipboard(text) catch |e| - self.logger.print_err("clipboard", "failed to set clipboard: {any}", .{e}); - } else { - self.rdr_.copy_to_system_clipboard(text); - } + if (self.clipboard) |old| + self.allocator.free(old); + self.clipboard = text; }