//! helix's registers: named stores of text, each holding one VALUE PER RANGE //! of the selection that wrote it. `"` is the default, `+` the one the //! system clipboard is copied from, `/` the last s/S pattern, `@` the last //! macro; any other character names a register of its own. The registers //! computed from the editor rather than stored (`_`, `#`, `.`, `%`) and the //! clipboard's paste side are read in edit.zig, which has the pane at hand. const std = @import("std"); const Registers = @This(); list: std.ArrayList(Reg) = .empty, pub const Reg = struct { name: u21, /// gpa-owned, one per range, in document order values: [][]u8, /// the values joined by newlines, for the readers that take one text /// (the acme chords, a paste into a terminal, the clipboard); gpa-owned, /// made on first read joined: ?[]u8 = null, }; pub const default = '"'; pub fn deinit(r: *Registers, gpa: std.mem.Allocator) void { for (r.list.items) |*reg| freeReg(gpa, reg); r.list.deinit(gpa); r.* = .{}; } fn freeReg(gpa: std.mem.Allocator, reg: *Reg) void { for (reg.values) |v| gpa.free(v); gpa.free(reg.values); if (reg.joined) |j| gpa.free(j); } pub fn get(r: *Registers, name: u21) ?*Reg { for (r.list.items) |*reg| if (reg.name == name) return reg; return null; } /// Value `i` of register `name`: a register holding fewer values than there /// are ranges repeats its last (helix paste_impl). pub fn value(r: *Registers, name: u21, i: usize) ?[]const u8 { const reg = r.get(name) orelse return null; if (reg.values.len == 0) return null; return reg.values[@min(i, reg.values.len - 1)]; } /// Store value `i` of `n`. `fresh` starts the register over with `n` empty /// values; the replay across ranges passes it on its first pass only, so /// every range fills its own slot. pub fn put(r: *Registers, gpa: std.mem.Allocator, name: u21, bytes: []const u8, i: usize, n: usize, fresh: bool) void { const copy = gpa.dupe(u8, bytes) catch return; const reg = r.get(name) orelse blk: { r.list.append(gpa, .{ .name = name, .values = &.{} }) catch { gpa.free(copy); return; }; break :blk &r.list.items[r.list.items.len - 1]; }; if (fresh or reg.values.len != n) { const values = gpa.alloc([]u8, n) catch { gpa.free(copy); return; }; for (values) |*v| v.* = &.{}; freeReg(gpa, reg); reg.* = .{ .name = name, .values = values }; } gpa.free(reg.values[i]); reg.values[i] = copy; if (reg.joined) |j| gpa.free(j); reg.joined = null; } /// The whole register as one text: its values joined by newlines, no /// newline added after a value that already ends in one. pub fn text(r: *Registers, gpa: std.mem.Allocator, name: u21) ?[]const u8 { const reg = r.get(name) orelse return null; if (reg.joined) |j| return j; var out: std.ArrayList(u8) = .empty; for (reg.values, 0..) |v, i| { if (i > 0 and !std.mem.endsWith(u8, reg.values[i - 1], "\n")) out.append(gpa, '\n') catch return null; out.appendSlice(gpa, v) catch return null; } reg.joined = out.toOwnedSlice(gpa) catch return null; return reg.joined; } test "a register holds a value per range and joins them for one-text readers" { const gpa = std.testing.allocator; var r: Registers = .{}; defer r.deinit(gpa); try std.testing.expect(r.value(default, 0) == null); // a replay fills the last range first r.put(gpa, default, "two", 1, 2, true); r.put(gpa, default, "one\n", 0, 2, false); try std.testing.expectEqualStrings("one\n", r.value(default, 0).?); try std.testing.expectEqualStrings("two", r.value(default, 1).?); try std.testing.expectEqualStrings("two", r.value(default, 5).?); try std.testing.expectEqualStrings("one\ntwo", r.text(gpa, default).?); r.put(gpa, 'a', "x", 0, 1, true); try std.testing.expectEqualStrings("x", r.text(gpa, 'a').?); try std.testing.expectEqualStrings("one\ntwo", r.text(gpa, default).?); }