summaryrefslogtreecommitdiff
path: root/src/recent.zig
blob: b6a1a66a4681de391194499cdefa0a122b56d2d5 (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
//! 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,
};

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 {
        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.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> <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} {s}\n", .{ e.line, e.col, 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;
            var it = std.mem.splitScalar(u8, line, ' ');
            const ln = std.fmt.parseInt(u32, it.next() orelse continue, 10) catch continue;
            const col = std.fmt.parseInt(u32, it.next() orelse continue, 10) catch continue;
            const path = it.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 = ln, .col = col }) 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 /a\n0 0 /b\n", w.buffered());
    var again: List = .{};
    defer again.deinit(gpa);
    again.read(gpa, w.buffered());
    again.read(gpa, "junk\n");
    try std.testing.expectEqual(@as(usize, 2), 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);
}