diff --git a/src/tui/editor.zig b/src/tui/editor.zig index f4c97c3..dd3c8c8 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -2675,6 +2675,7 @@ pub const Editor = struct { var all_stop = true; var root = root_; + tui.clipboard_start_group(); 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; @@ -2696,6 +2697,7 @@ pub const Editor = struct { const primary = self.get_primary(); const b = self.buf_for_update() catch return; var root = b.root; + tui.clipboard_start_group(); if (self.cursels.items.len == 1 and primary.selection == null) try self.select_line_at_cursor(root, primary, .include_eol); for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { @@ -2713,15 +2715,14 @@ pub const Editor = struct { var root = b.root; if (self.cursels.items.len == 1 and primary.selection == null) try self.select_line_at_cursor(root, primary, .include_eol); - var count: usize = 0; + tui.clipboard_start_group(); 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); }; try self.update_buf(root); self.clamp(); - try tui.clipboard_send_to_system(count); + try tui.clipboard_send_to_system(); } pub const cut_meta: Meta = .{ .description = "Cut selection or current line to clipboard" }; @@ -2735,18 +2736,16 @@ 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; + tui.clipboard_start_group(); 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)); }; - return tui.clipboard_send_to_system(count); + return tui.clipboard_send_to_system(); } pub const copy_meta: Meta = .{ .description = "Copy selection to clipboard" }; - fn copy_cursel_file_name(self: *const Self) error{OutOfMemory}!usize { + fn copy_cursel_file_name(self: *const Self) error{OutOfMemory}!void { tui.clipboard_add_chunk(try tui.clipboard_allocator().dupe(u8, self.file_path orelse "*")); - return 1; } fn copy_cursel_file_name_and_location(self: *const Self, cursel: *const CurSel) error{ WriteFailed, OutOfMemory }!void { @@ -2779,23 +2778,20 @@ pub const Editor = struct { 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; + fn copy_cursels_file_name_and_location(self: *const Self) error{OutOfMemory}!void { + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| 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; _ = ctx.args.match(.{tp.extract(&mode)}) catch false; - const n = switch (mode) { + tui.clipboard_start_group(); + 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); + } + return tui.clipboard_send_to_system(); } pub const copy_file_name_meta: Meta = .{ .description = "Copy file name and location to clipboard", @@ -2803,6 +2799,7 @@ pub const Editor = struct { pub fn copy_internal_vim(self: *Self, _: Context) Result { const root = self.buf_root() catch return; + tui.clipboard_start_group(); 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)); } @@ -2817,49 +2814,69 @@ pub const Editor = struct { try move_cursor_end(root, &sel.end, self.metrics); try move_cursor_right(root, &sel.end, self.metrics); } + tui.clipboard_start_group(); 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)); } 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_} + var group_idx: usize = 0; + var text_: []const u8 = &.{}; + const clipboard: []const tui.ClipboardEntry = if (try ctx.args.match(.{tp.extract(&group_idx)})) blk: { + self.logger.print("paste: pasting group {d}", .{group_idx}); + break :blk tui.clipboard_get_group(group_idx); + } else if (try ctx.args.match(.{tp.extract(&text_)})) + &[_]tui.ClipboardEntry{.{ .text = text_ }} else - tui.clipboard_get_history() orelse return; + tui.clipboard_get_group(0); 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 (cursel_idx == 0) break; - cursel_idx -= 1; + + if (clipboard.len == 0) { + self.logger.print("paste: nothing to paste", .{}); + return; } + + if (clipboard.len > 1 and self.cursels.items.len == 1) { + const cursel = self.get_primary(); + for (clipboard) |item| { + root = try self.insert(root, cursel, item.text, b.allocator); + if (item.text[item.text.len - 1] != '\n') + root = try self.insert(root, cursel, "\n", b.allocator); + } + } else { + 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].text; + root = try self.insert(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; + } + } + self.logger.print("paste: {d} bytes", .{bytes}); try self.update_buf(root); self.clamp(); self.need_render(); } - pub const paste_meta: Meta = .{ .description = "Paste from internal clipboard" }; + pub const paste_meta: Meta = .{ .description = "Paste from internal clipboard", .arguments = &.{.integer} }; 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_} + const clipboard: []const tui.ClipboardEntry = if (ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text_)})) + &[_]tui.ClipboardEntry{.{ .text = text_ }} else - tui.clipboard_get_history() orelse return; + tui.clipboard_get_group(0); const b = try self.buf_for_update(); var root = b.root; @@ -2870,7 +2887,7 @@ pub const Editor = struct { while (true) { const cursel_ = &self.cursels.items[cursel_idx]; if (cursel_.*) |*cursel| { - const text = clipboard[idx]; + const text = clipboard[idx].text; root = try self.insert_line_vim(root, cursel, text, b.allocator); idx = if (idx == 0) clipboard.len - 1 else idx - 1; bytes += text.len; diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 2dcaaa3..c171a6f 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1409,7 +1409,11 @@ pub fn write_restore_info(self: *Self) void { if (tui.clipboard_get_history()) |clipboard| { cbor.writeArrayHeader(writer, clipboard.len) catch return; - for (clipboard) |item| cbor.writeValue(writer, item) catch return; + for (clipboard) |item| { + cbor.writeArrayHeader(writer, 2) catch return; + cbor.writeValue(writer, item.group) catch return; + cbor.writeValue(writer, item.text) catch return; + } } else { cbor.writeValue(writer, null) catch return; } @@ -1443,11 +1447,18 @@ fn read_restore_info(self: *Self) !void { tui.clipboard_clear_all(); var len = try cbor.decodeArrayHeader(&iter); + var prev_group: usize = 0; 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)); + const len_ = try cbor.decodeArrayHeader(&iter); + if (len_ != 2) return error.Stop; + var group: usize = 0; + var text: []const u8 = undefined; + if (!try cbor.matchValue(&iter, cbor.extract(&group))) return error.Stop; + if (!try cbor.matchValue(&iter, cbor.extract(&text))) return error.Stop; + if (prev_group != group) tui.clipboard_start_group(); + prev_group = group; + tui.clipboard_add_chunk(try clipboard_allocator.dupe(u8, text)); } try self.buffer_manager.extract_state(&iter); diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 06fef1d..f7ec14d 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -307,7 +307,7 @@ const cmds_ = struct { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; const b = try ed.buf_for_update(); - tui.clipboard_clear_all(); + tui.clipboard_start_group(); const root = try ed.cut_to(move_noop, b.root); try ed.update_buf(root); ed.clamp(); @@ -418,7 +418,7 @@ const cmds_ = struct { const ed = mv.get_active_editor() orelse return; const root = ed.buf_root() catch return; - tui.clipboard_clear_all(); + tui.clipboard_start_group(); 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)); @@ -890,10 +890,10 @@ fn paste_helix(ctx: command.Context, do_paste: pasting_function) command.Result 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_} + const clipboard: []const tui.ClipboardEntry = if (ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text_)})) + &[_]tui.ClipboardEntry{.{ .text = text_ }} else - tui.clipboard_get_history() orelse return; + tui.clipboard_get_group(0); const b = try ed.buf_for_update(); var root = b.root; @@ -905,11 +905,11 @@ fn paste_helix(ctx: command.Context, do_paste: pasting_function) command.Result 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; + root = try do_paste(ed, root, cursel, clipboard[idx].text, b.allocator); + bytes += clipboard[idx].text.len; } else { - bytes += clipboard[clipboard.len - 1].len; - root = try do_paste(ed, root, cursel, clipboard[clipboard.len - 1], b.allocator); + bytes += clipboard[clipboard.len - 1].text.len; + root = try do_paste(ed, root, cursel, clipboard[clipboard.len - 1].text, b.allocator); } }; ed.logger.print("paste: {d} bytes", .{bytes}); diff --git a/src/tui/mode/overlay/clipboard_palette.zig b/src/tui/mode/overlay/clipboard_palette.zig index c4b770e..acb9a3f 100644 --- a/src/tui/mode/overlay/clipboard_palette.zig +++ b/src/tui/mode/overlay/clipboard_palette.zig @@ -16,6 +16,7 @@ pub const icon = " "; pub const Entry = struct { label: []const u8, idx: usize, + group: usize, }; pub fn load_entries(palette: *Type) !usize { @@ -24,7 +25,8 @@ pub fn load_entries(palette: *Type) !usize { if (history.len > 0) { var idx = history.len - 1; while (true) : (idx -= 1) { - var label_ = history[idx]; + const entry = &history[idx]; + var label_ = entry.text; while (label_.len > 0) switch (label_[0]) { ' ', '\t', '\n' => label_ = label_[1..], else => break, @@ -32,6 +34,7 @@ pub fn load_entries(palette: *Type) !usize { (try palette.entries.addOne(palette.allocator)).* = .{ .label = label_, .idx = idx, + .group = entry.group, }; if (idx == 0) break; } @@ -51,7 +54,11 @@ pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !v var hint: std.Io.Writer.Allocating = .init(palette.allocator); defer hint.deinit(); - const item = if (tui.clipboard_get_history()) |clipboard| clipboard[entry.idx] else &.{}; + const clipboard_ = tui.clipboard_get_history(); + const clipboard = clipboard_ orelse &.{}; + const clipboard_entry: tui.ClipboardEntry = if (clipboard_) |_| clipboard[entry.idx] else .{}; + const group_idx = tui.clipboard_current_group() - clipboard_entry.group; + const item = clipboard_entry.text; var line_count: usize = 1; for (0..item.len) |i| if (item[i] == '\n') { line_count += 1; @@ -60,6 +67,7 @@ pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !v 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 hint.writer.print(":{d}", .{group_idx}); try cbor.writeValue(writer, hint.written()); try cbor.writeValue(writer, matches orelse &[_]usize{}); diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 4d2dca6..45063d6 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -76,10 +76,16 @@ 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: ?std.ArrayList(ClipboardEntry) = null, +clipboard_current_group_number: usize = 0, color_scheme: enum { dark, light } = .dark, color_scheme_locked: bool = false, +pub const ClipboardEntry = struct { + text: []const u8 = &.{}, + group: usize = 0, +}; + const keepalive = std.time.us_per_day * 365; // one year const idle_frames = 0; const mouse_idle_time_milliseconds = 3000; @@ -377,10 +383,7 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { var text: []const u8 = undefined; if (try m.match(.{ "system_clipboard", tp.extract(&text) })) { try self.dispatch_flush_input_event(); - return if (command.get_id("mini_mode_paste")) |id| - command.execute(id, command.fmt(.{text})) - else - command.executeName("paste", command.fmt(.{text})); + return self.handle_system_clipboard(text); } if (try m.match(.{ "system_clipboard", tp.null_ })) @@ -605,6 +608,20 @@ fn dispatch_event(ctx: *anyopaque, cbor_msg: []const u8) void { tp.self_pid().send_raw(m) catch |e| self.logger.err("dispatch event", e); } +fn handle_system_clipboard(self: *Self, text: []const u8) !void { + if (command.get_id("mini_mode_paste")) |id| + return command.execute(id, command.fmt(.{text})); + + { + const text_ = try clipboard_system_clipboard_text(self.allocator); + defer self.allocator.free(text_); + if (std.mem.eql(u8, text_, text)) + return command.executeName("paste", command.fmt(.{0})); + } + + return command.executeName("paste", command.fmt(.{text})); +} + fn find_coord_widget(self: *Self, y: usize, x: usize) ?*Widget { const Ctx = struct { widget: ?*Widget = null, @@ -1302,7 +1319,7 @@ const cmds = struct { return error.InvalidClipboardDeleteArgument; const clipboard = if (self.clipboard) |*clipboard| clipboard else return; const removed = clipboard.orderedRemove(idx); - self.allocator.free(removed); + self.allocator.free(removed.text); } pub const clipboard_delete_meta: Meta = .{}; }; @@ -1901,7 +1918,7 @@ 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); + self.allocator.free(chunk.text); clipboard.deinit(self.allocator); } self.clipboard = null; @@ -1912,7 +1929,7 @@ pub fn clipboard_allocator() Allocator { return self.allocator; } -pub fn clipboard_get_history() ?[]const []const u8 { +pub fn clipboard_get_history() ?[]const ClipboardEntry { const self = current(); return if (self.clipboard) |clipboard| clipboard.items else null; } @@ -1920,12 +1937,55 @@ pub fn clipboard_get_history() ?[]const []const u8 { pub fn clipboard_peek_chunk() ?[]const u8 { const self = current(); const clipboard = self.clipboard orelse return null; - return clipboard[clipboard.len - 1]; + return clipboard[clipboard.len - 1].text; +} + +pub fn clipboard_start_group() void { + const self = current(); + self.clipboard_current_group_number += 1; +} + +pub fn clipboard_current_group() usize { + const self = current(); + return self.clipboard_current_group_number; +} + +pub fn clipboard_group_size(group: usize) usize { + const self = current(); + const clipboard = self.clipboard orelse return 0; + var count: usize = 0; + var idx: usize = clipboard.len - 1; + while (clipboard[idx].group != group) { + if (idx == 0) return 0 else idx -= 1; + } + while (clipboard[idx].group == group) { + count += 1; + if (idx == 0) break else idx -= 1; + } + return count; +} + +pub fn clipboard_get_group(group_idx: usize) []const ClipboardEntry { + const self = current(); + const clipboard = (self.clipboard orelse return &.{}).items; + const group = self.clipboard_current_group_number - group_idx; + if (clipboard.len == 0) return &.{}; + var group_end: usize = clipboard.len; + while (clipboard[group_end - 1].group != group) { + if (group_end == 1) return &.{} else group_end -= 1; + } + var group_begin: usize = group_end; + while (clipboard[group_begin - 1].group == group) { + group_begin -= 1; + if (group_begin == 0) break; + } + return clipboard[group_begin..group_end]; } pub fn clipboard_clear_all() void { const self = current(); self.clipboard_deinit(); + self.clipboard_current_group_number = 0; } pub fn clipboard_add_chunk(text: []const u8) void { @@ -1935,23 +1995,28 @@ pub fn clipboard_add_chunk(text: []const u8) void { break :blk &self.clipboard.?; }; const chunk = clipboard.addOne(self.allocator) catch @panic("OOM clipboard_add_chunk"); - chunk.* = text; + chunk.text = text; + chunk.group = self.clipboard_current_group_number; } -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); +fn clipboard_system_clipboard_text(allocator: std.mem.Allocator) error{ Stop, WriteFailed, OutOfMemory }![]const u8 { + var buffer: std.Io.Writer.Allocating = .init(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]); + const clipboard = clipboard_get_group(0); var first = true; - const chunks = clipboard[clipboard.len - n_chunks ..]; - for (chunks) |chunk| { + for (clipboard) |chunk| { if (first) first = false else try writer.writeByte('\n'); - try writer.writeAll(chunk); + try writer.writeAll(chunk.text); } + return buffer.toOwnedSlice(); +} + +pub fn clipboard_send_to_system() error{ Stop, WriteFailed, OutOfMemory }!void { + const self = current(); + const text = try clipboard_system_clipboard_text(self.allocator); + defer self.allocator.free(text); + return self.clipboard_send_to_system_internal(text); } fn clipboard_send_to_system_internal(self: *Self, text: []const u8) void {