diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 104 |
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); |
