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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
//! 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,
/// Open in a pane now (opened, not closed since): never the one the
/// cap drops. ponytail: a pane renamed away leaves its old path marked
/// open until the session ends; kept rather than tracked by pane.
open: bool = false,
};
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,
};
entry.open = true;
// The oldest closed file goes for it; an open one never does.
if (l.entries.items.len >= cap) {
var i = l.entries.items.len;
while (i > 0) : (i -= 1) if (!l.entries.items[i - 1].open) {
gpa.free(l.entries.orderedRemove(i - 1).path);
break;
};
}
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.entries.items[i].open = false;
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 {
// A name with a newline in it would read back as two lines: not kept.
for (l.entries.items) |e| if (std.mem.indexOfScalar(u8, e.path, '\n') == null)
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;
const path = std.fmt.bufPrint(&name, "/f{d}", .{i}) catch unreachable;
l.opened(gpa, path);
l.closed(gpa, path, 1, 1);
}
try std.testing.expectEqual(@as(usize, cap), l.entries.items.len);
}
test "the cap drops the oldest closed file, never an open one" {
const gpa = std.testing.allocator;
var l: List = .{};
defer l.deinit(gpa);
var name: [16]u8 = undefined;
// The oldest is open; the next is closed.
l.opened(gpa, "/open");
l.opened(gpa, "/shut");
l.closed(gpa, "/shut", 1, 1);
for (2..cap) |i| l.opened(gpa, std.fmt.bufPrint(&name, "/f{d}", .{i}) catch unreachable);
try std.testing.expectEqual(@as(usize, cap), l.entries.items.len);
l.opened(gpa, "/new");
try std.testing.expectEqual(@as(usize, cap), l.entries.items.len);
try std.testing.expect(l.get("/open") != null);
try std.testing.expect(l.get("/shut") == null);
}
|