summaryrefslogtreecommitdiff
path: root/src/recent.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 10:30:38 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit2d0c506583d18d6f004fc117282deb2faea1d17e (patch)
tree683d1dd2820df31222360c0308fb3333b64aae1e /src/recent.zig
parentf207f1d4d56d1012766e6206e713f2b3da70579d (diff)
downloadpardes-2d0c506583d18d6f004fc117282deb2faea1d17e.tar.gz
pardes-2d0c506583d18d6f004fc117282deb2faea1d17e.zip
Recent lists the files opened lately, closed ones too, and a jump back to a closed file reopens it
A file closed by accident, its path forgotten, could not be found again: the jumplist dropped closed panes. Every file opened (a look, a Save of a scratch, a Restore, a rename) goes first in a list of 200, kept once, its dot noted when it closes, kept across sessions in $XDG_STATE_HOME/pardes/ recent. `Recent` (SPC f r) shows it in a reused +Recent, each row `path:line:col open|closed` a look reopens; /recent reads `open|closed <path>`. The jumplist keeps a closed file's entries, +Jumps marks them `(closed)`, and Back to one opens the file at its place. acme has no such thing; its dump and Load are the nearest. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/recent.zig')
-rw-r--r--src/recent.zig119
1 files changed, 119 insertions, 0 deletions
diff --git a/src/recent.zig b/src/recent.zig
new file mode 100644
index 00000000..b6a1a66a
--- /dev/null
+++ b/src/recent.zig
@@ -0,0 +1,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);
+}