summaryrefslogtreecommitdiff
path: root/src/recent.zig
blob: 9f18d737e2152820c14124caea959125f6dda897 (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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! The files opened lately, most recent first, those since closed too, as
//! `Recent` lists them and a jump to a closed pane reopens them: a path
//! and where the dot was when it closed. Kept across sessions in a small
//! file under $XDG_STATE_HOME/pardes (Options.recent_path) when the host
//! gives one. acme has nothing like it (its dump and Load are the nearest):
//! a file closed by accident, its path forgotten, is found here again.

const std = @import("std");

/// At most this many files are remembered, the oldest dropped first.
pub const cap = 200;

pub const Entry = struct {
    path: []u8,
    /// Where its dot was when it last closed (1-based; 0 none).
    line: u32 = 0,
    col: u32 = 0,
    /// The selection's other end then (its anchor), 0 when there was none.
    aline: u32 = 0,
    acol: u32 = 0,
};

pub const List = struct {
    entries: std.ArrayList(Entry) = .empty,
    /// Changed since it was last written out.
    dirty: bool = false,

    pub fn deinit(l: *List, gpa: std.mem.Allocator) void {
        for (l.entries.items) |e| gpa.free(e.path);
        l.entries.deinit(gpa);
    }

    fn find(l: *const List, path: []const u8) ?usize {
        for (l.entries.items, 0..) |e, i| if (std.mem.eql(u8, e.path, path)) return i;
        return null;
    }

    /// `path` opened: first in the list, once, keeping the dot it closed at.
    pub fn opened(l: *List, gpa: std.mem.Allocator, path: []const u8) void {
        if (path.len == 0) return;
        var entry: Entry = if (l.find(path)) |i| l.entries.orderedRemove(i) else .{
            .path = gpa.dupe(u8, path) catch return,
        };
        if (l.entries.items.len >= cap) {
            const last = l.entries.pop().?;
            gpa.free(last.path);
        }
        l.entries.insert(gpa, 0, entry) catch {
            gpa.free(entry.path);
            entry = undefined;
            return;
        };
        l.dirty = true;
    }

    /// `path` closed with its dot at `line`:`col`: where a reopen lands.
    /// Its place in the list is its opening's.
    pub fn closed(l: *List, gpa: std.mem.Allocator, path: []const u8, line: u32, col: u32) void {
        l.closedSel(gpa, path, line, col, 0, 0);
    }

    /// `closed`, with the selection's anchor too (0 0 for none).
    pub fn closedSel(l: *List, gpa: std.mem.Allocator, path: []const u8, line: u32, col: u32, aline: u32, acol: u32) void {
        const i = l.find(path) orelse found: {
            l.opened(gpa, path);
            // Out of memory for it: nothing to note the dot on.
            break :found l.find(path) orelse return;
        };
        l.entries.items[i].line = line;
        l.entries.items[i].col = col;
        l.entries.items[i].aline = aline;
        l.entries.items[i].acol = acol;
        l.dirty = true;
    }

    pub fn get(l: *const List, path: []const u8) ?Entry {
        return l.entries.items[l.find(path) orelse return null];
    }

    /// One line a file, `<line> <col> <aline> <acol> <path>`, most recent
    /// first.
    pub fn write(l: *const List, w: *std.Io.Writer) !void {
        for (l.entries.items) |e| try w.print("{d} {d} {d} {d} {s}\n", .{ e.line, e.col, e.aline, e.acol, e.path });
    }

    /// The lines `write` wrote; a line that is not one is passed over.
    pub fn read(l: *List, gpa: std.mem.Allocator, bytes: []const u8) void {
        var lines = std.mem.splitScalar(u8, bytes, '\n');
        while (lines.next()) |line| {
            if (l.entries.items.len >= cap) break;
            // Numbers, then the path (which starts with `/`): two of them in
            // a list written before the anchor was kept, four since.
            var nums: [4]u32 = @splat(0);
            var n: usize = 0;
            var rest = line;
            while (rest.len > 0 and rest[0] != '/' and n < nums.len) : (n += 1) {
                const sp = std.mem.indexOfScalar(u8, rest, ' ') orelse break;
                nums[n] = std.fmt.parseInt(u32, rest[0..sp], 10) catch break;
                rest = rest[sp + 1 ..];
            }
            if (n != 2 and n != 4) continue;
            const path = rest;
            if (path.len == 0 or path[0] != '/' or l.find(path) != null) continue;
            const owned = gpa.dupe(u8, path) catch return;
            l.entries.append(gpa, .{ .path = owned, .line = nums[0], .col = nums[1], .aline = nums[2], .acol = nums[3] }) catch {
                gpa.free(owned);
                return;
            };
        }
    }
};

test "the list keeps a path once, most recent first, its dot from closing, capped" {
    const gpa = std.testing.allocator;
    var l: List = .{};
    defer l.deinit(gpa);
    l.opened(gpa, "/a");
    l.opened(gpa, "/b");
    l.closed(gpa, "/a", 7, 3);
    l.opened(gpa, "/a");
    try std.testing.expectEqualStrings("/a", l.entries.items[0].path);
    try std.testing.expectEqual(@as(usize, 2), l.entries.items.len);
    try std.testing.expectEqual(@as(u32, 7), l.get("/a").?.line);
    var buf: [256]u8 = undefined;
    var w = std.Io.Writer.fixed(&buf);
    try l.write(&w);
    try std.testing.expectEqualStrings("7 3 0 0 /a\n0 0 0 0 /b\n", w.buffered());
    var again: List = .{};
    defer again.deinit(gpa);
    again.read(gpa, w.buffered());
    again.read(gpa, "junk\n");
    // A list written before the anchor was kept still reads.
    again.read(gpa, "4 2 /old\n");
    try std.testing.expectEqual(@as(u32, 4), again.get("/old").?.line);
    try std.testing.expectEqual(@as(usize, 3), again.entries.items.len);
    for (0..cap + 5) |i| {
        var name: [16]u8 = undefined;
        l.opened(gpa, std.fmt.bufPrint(&name, "/f{d}", .{i}) catch unreachable);
    }
    try std.testing.expectEqual(@as(usize, cap), l.entries.items.len);
}