//! 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, ` `, 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); }