summaryrefslogtreecommitdiff
path: root/src/Registers.zig
blob: d2a4243462dca4932b39fda0ed3686f44254b792 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! 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).?);
}