summaryrefslogtreecommitdiff
path: root/src/pardes.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/pardes.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/pardes.zig')
-rw-r--r--src/pardes.zig104
1 files changed, 101 insertions, 3 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index f7aee16c..33aff840 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -14,6 +14,7 @@ const tracy = @import("tracy.zig");
pub const panes = @import("panes.zig");
pub const tagline = @import("tagline.zig");
pub const exec = @import("exec.zig");
+const recent_files = @import("recent.zig");
pub const mouse = @import("mouse.zig");
pub const normal = @import("normal.zig");
pub const edit = @import("edit.zig");
@@ -447,11 +448,17 @@ test "a compact tagline anchors at its own pane, and both shells step from the s
try std.testing.expectEqual(@as(u16, 3), gridColAt(p, 3.5 * body_w, body_row, body_w, tag_w));
}
+const recent_path_cap = limits.host_path_cap;
+
pub const Loc = struct {
pane: u16,
+ /// 0 once its pane has closed: a file's entry stays, reopened by a
+ /// jump to it from `path_hash`'s recent entry.
serial: u32,
line: u32,
col: u32,
+ /// The file it is in (Wyhash of the path), 0 for a pane with none.
+ path_hash: u64 = 0,
};
test {
@@ -474,6 +481,7 @@ test {
_ = @import("draw.zig");
_ = @import("tag_layer.zig");
_ = @import("dump.zig"); // its tests ran nowhere before
+ _ = @import("recent.zig");
_ = @import("Presentation.zig");
_ = @import("animation.zig");
}
@@ -4232,6 +4240,10 @@ pub const Options = struct {
ninep_quic: ?[]const u8 = null,
mounts: []const filesystem.Mount = &.{},
config_dir: ?[]const u8 = null,
+ /// Where the list of files opened lately is kept across sessions
+ /// (recent.zig), set by the host that has a state directory; none, it
+ /// lives as long as the session.
+ recent_path: ?[]const u8 = null,
startup_config_path: ?[]const u8 = null,
image_allocator: ?std.mem.Allocator = null,
pdf_allocator: ?std.mem.Allocator = null,
@@ -4298,6 +4310,12 @@ pub const Pardes = struct {
presentation: Presentation = .{},
active: usize = 0,
+ /// The files opened lately, closed ones too (recent.zig): `Recent`,
+ /// /recent, and the jumplist's way back to a closed pane.
+ recent: recent_files.List = .{},
+ /// The kept list was read (or there was none): only then is it written,
+ /// so a session never writes over one it did not read.
+ recent_loaded: bool = false,
jumps: [MAX_JUMPS]Loc = undefined,
njumps: usize = 0,
jcur: usize = 0,
@@ -4621,6 +4639,7 @@ pub const Pardes = struct {
},
}
p.sync();
+ p.loadRecent();
exec.applyStartupConfig(p);
colors.finishThemeInitialization(p);
p.sync();
@@ -4641,7 +4660,58 @@ pub const Pardes = struct {
p.col_panes[0][0] = 0;
}
+ /// The recent list's own allocator: it is kept best effort (a file not
+ /// remembered for want of memory is no failure of what opened it), so
+ /// on a host it takes from the process heap, not the editor's, whose
+ /// every failure must be answered.
+ // ponytail: a host heap for a best-effort list; the board's gpa there.
+ pub fn recentGpa(p: *const Pardes) std.mem.Allocator {
+ return if (comptime hosted) std.heap.smp_allocator else p.gpa;
+ }
+
+ /// The kept list of files opened lately, read once at start.
+ fn loadRecent(p: *Pardes) void {
+ const path = p.opts.recent_path orelse return;
+ if (comptime !hosted) return;
+ defer p.recent_loaded = true;
+ const bytes = filesystem.readFile(p.gpa, path) catch return;
+ defer p.gpa.free(bytes);
+ p.recent.read(p.recentGpa(), bytes);
+ }
+
+ /// Written out when it changed, at the end of a step and at the end.
+ pub fn saveRecent(p: *Pardes) void {
+ if (!p.recent.dirty) return;
+ p.recent.dirty = false;
+ const path = p.opts.recent_path orelse return;
+ if (comptime !hosted) return;
+ if (!p.recent_loaded) return;
+ var out: std.Io.Writer.Allocating = .init(p.gpa);
+ defer out.deinit();
+ p.recent.write(&out.writer) catch return;
+ turn.yield();
+ defer turn.back();
+ std.Io.Dir.cwd().writeFile(std.Io.Threaded.global_single_threaded.io(), .{ .sub_path = path, .data = out.written() }) catch {};
+ }
+
+ /// A file pane's path and dot, for the recent list (0 when none).
+ fn recentDot(pane: *const Pane) ?struct { path: []const u8, line: u32, col: u32 } {
+ const f = pane.file orelse return null;
+ if (f.output != null) return null;
+ return .{ .path = f.path, .line = @intCast(@max(0, pane.body.cur_row) + 1), .col = @intCast(@max(0, pane.body.cur_col) + 1) };
+ }
+
+ /// The recent entry a closed jump names, by its path's hash.
+ pub fn recentByHash(p: *const Pardes, hash: u64) ?recent_files.Entry {
+ for (p.recent.entries.items) |e| if (std.hash.Wyhash.hash(0, e.path) == hash) return e;
+ return null;
+ }
+
pub fn deinit(p: *Pardes) void {
+ // Every file open now is closed with its dot, and the list kept.
+ for (p.panes) |slot| if (slot) |pane| if (recentDot(pane)) |d| p.recent.closed(p.recentGpa(), d.path, d.line, d.col);
+ p.saveRecent();
+ p.recent.deinit(p.recentGpa());
look.cancelLookHover(p);
if (p.repl_ask) |ask| p.gpa.free(ask.text);
for (0..MAX_PANES) |id| p.dropPendingWrite(id);
@@ -4750,6 +4820,9 @@ pub const Pardes = struct {
}
fn retirePane(p: *Pardes, pane: *Pane) void {
+ // Closed, a file is remembered with its dot: Recent and a jump back
+ // to it reopen it there.
+ if (recentDot(pane)) |d| p.recent.closed(p.recentGpa(), d.path, d.line, d.col);
const watched = (if (pane.file) |f| f.output == null else false) or pane.hasPdf();
for (p.panes, 0..) |slot, id| if (slot == pane) {
if (watched) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = false } });
@@ -5240,6 +5313,7 @@ pub const Pardes = struct {
f.saved_revision = f.revision;
f.disk_gone = false;
ctlfs.events.noteLog(p, .save, pane);
+ p.recent.opened(p.recentGpa(), f.path);
if (filesystem.localPath(f.path) != null)
p.emit(.{ .watch = .{ .pane = id, .on = true } });
}
@@ -6562,9 +6636,15 @@ pub const Pardes = struct {
var cur: usize = 0;
for (p.jumps[0..p.njumps], 0..) |j, i| {
if (i == p.jcur) cur = w; // survives -> lands at w; dies -> the next one does
- const pane = p.panes[j.pane] orelse continue;
- if (pane.serial != j.serial) continue;
- p.jumps[w] = j;
+ var kept = j;
+ const live = if (j.serial == 0) false else if (p.panes[j.pane]) |pane| pane.serial == j.serial else false;
+ // A file's pane closed: its entry stays, closed, while the recent
+ // list knows the file, and a jump to it opens it again.
+ if (!live) {
+ if (j.path_hash == 0 or p.recentByHash(j.path_hash) == null) continue;
+ kept.serial = 0;
+ }
+ p.jumps[w] = kept;
w += 1;
}
p.njumps = w;
@@ -6576,6 +6656,7 @@ pub const Pardes = struct {
.serial = pane.serial,
.line = if (pane.pdfPage()) |page| @intCast(page + 1) else if (pane.body.cur_pinned) @intCast(@max(0, pane.body.cur_row) + 1) else 0,
.col = if (pane.pdfPage() != null) 0 else if (pane.body.cur_pinned) @intCast(@max(0, pane.body.cur_col) + 1) else 0,
+ .path_hash = if (recentDot(pane)) |d| std.hash.Wyhash.hash(0, d.path) else 0,
};
if (p.njumps > 0 and p.samePlace(p.jumps[p.jcur], now)) {
p.jumps[p.jcur] = now; // same visit; the entry IS where you are
@@ -6604,6 +6685,21 @@ pub const Pardes = struct {
if (p.njumps == 0 or next < 0 or next >= p.njumps) return;
const j = p.jumps[@intCast(next)];
if (j.pane >= MAX_PANES) return;
+ // A closed file's entry: opened again at its dot, placed as a look
+ // places it, and the entry is that pane from here on.
+ if (j.serial == 0) {
+ const e = p.recentByHash(j.path_hash) orelse return;
+ var where: [recent_path_cap + 32]u8 = undefined;
+ const loc = std.fmt.bufPrint(&where, "{s}:{d}:{d}", .{ e.path, @max(1, j.line), @max(1, j.col) }) catch return;
+ const from = p.active;
+ look.lookAt(p, from, loc);
+ const opened = p.panes[p.active] orelse return;
+ if (p.active == from) return;
+ p.jumps[@intCast(next)].pane = @intCast(p.active);
+ p.jumps[@intCast(next)].serial = opened.serial;
+ p.jcur = @intCast(next);
+ return;
+ }
const pane = p.panes[j.pane] orelse return;
if (pane.serial != j.serial) return;
if (p.panes[p.active]) |source| look.clearNavigationSelection(p, source);
@@ -6640,6 +6736,8 @@ pub const Pardes = struct {
if (refused) for (0..p.ncol) |c| if (!layout.columnAtMinimums(p, c)) {
_ = layout.shareColumn(p, c);
};
+ // A file opened or closed this step: the kept list follows.
+ p.saveRecent();
tagline.flattenHeaders(p);
if (p.header_column != null and p.columnBarHeight() == 0) {
tagline.exitHeader(p);