diff options
| -rw-r--r-- | docs/fs.md | 7 | ||||
| -rw-r--r-- | src/Output.zig | 10 | ||||
| -rw-r--r-- | src/look.zig | 5 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 21 | ||||
| -rw-r--r-- | src/ninep/events.zig | 5 | ||||
| -rw-r--r-- | src/pardes.zig | 16 | ||||
| -rw-r--r-- | test/pdf.zig | 23 |
7 files changed, 78 insertions, 9 deletions
@@ -693,9 +693,10 @@ become spaces. - `/commands`: `Word [arg] root|pane|both [values] -- sentence`, one a line, generated from the builtin registry (`both`: Edit, at the active pane from the root). -- `/recent`: up to 200 files, most recent first, `open <path>` or `closed - <path>`; kept in `$XDG_STATE_HOME/pardes/recent`. `Recent` shows them in a - pane; a look at a row reopens the file at its last dot. +- `/recent`: up to 200 files, PDFs and images, most recent first, `open + <path>` or `closed <path>`; kept in `$XDG_STATE_HOME/pardes/recent`. + `Recent` shows them in a pane, an open one at its dot now and a closed + one at its last (a PDF's is its page); a look at a row reopens it there. - `/status`: `pid`, `version`, `panes`. - `/os/`: existing regular files take read, write and truncation to zero; create, remove, rename and metadata changes are refused; ownership is diff --git a/src/Output.zig b/src/Output.zig index 06f2f1dc..4aa0c021 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -612,10 +612,16 @@ pub fn openJumps(p: *Pardes, id: usize) !void { /// The recent list as rows: `<path>:<line>:<col> open|closed`, most recent /// first; a look at a row opens the file there, as a +Jumps row does. pub fn recentText(p: *Pardes, w: *std.Io.Writer, look_rows: bool) !void { - for (p.recent.entries.items) |e| { + for (p.recent.entries.items) |stored| { + // An open one's row is where it is now, not where it last closed. + var e = stored; var is_open = false; - for (p.panes) |slot| if (slot) |pane| if (pane.file) |f| if (f.output == null and std.mem.eql(u8, f.path, e.path)) { + for (p.panes) |slot| if (slot) |pane| if (Pardes.recentPlace(pane)) |d| if (std.mem.eql(u8, d.path, e.path)) { is_open = true; + e.line = d.line; + e.col = d.col; + e.aline = d.aline; + e.acol = d.acol; }; const state: []const u8 = if (is_open) "open" else "(closed)"; if (!look_rows) { diff --git a/src/look.zig b/src/look.zig index 2e8a4b39..36b91545 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1481,6 +1481,11 @@ pub fn reopenAt(p: *Pardes, from: usize, path: []const u8, at: Spot) void { lookAt(p, from, path); const id = p.active; const pane = p.panes[id] orelse return; + // A PDF's place is its page, the last if it has fewer now. + if (comptime pdf_enabled) if (pane.pdf) |*pv| if (std.mem.eql(u8, pv.path, path)) { + if (at.line > 0 and pv.focusLocation(p.pdf_gpa, @min(at.line, pv.page_count), 0)) panes.Pdf.resetPageChrome(pane); + return; + }; const f = if (pane.file) |*file| file else return; if (!std.mem.eql(u8, f.path, path)) return; // The empty "line" after a final newline is no line to land on. diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index eff8003b..75a7eb9a 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -3060,6 +3060,27 @@ test "a Jumplist entry is (closed) only while no pane has its file open" { for (p.jumps[0..p.njumps]) |j| try testing.expect(j.serial != 0); } +test "Recent's row for a file still open is where its dot is now, not where it last closed" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "a.txt", .data = "one\ntwo\nthree\n" }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + var line: [4200]u8 = undefined; + try testing.expectEqual(Status.ok, wr(p, root_look, try std.fmt.bufPrint(&line, "{s}/a.txt:3\n", .{dir})).reply.status); + p.sync(); + const pane = p.panes[p.active].?; + pane.body.vsel.active = false; // the look's selection of line 3 let go + pane.body.cur_row = 1; + pane.body.cur_col = 2; + try testing.expectEqual(Status.ok, wr(p, root_exec, "Recent\n").reply.status); + const rows = p.panes[p.paneBySerial(try std.fmt.parseInt(u32, std.mem.trimEnd(u8, rd(p, root_exec, 0, 64).bytes, "\n"), 10)).?].?; + var want: [4300]u8 = undefined; + try testing.expect(std.mem.indexOf(u8, rows.file.?.content, try std.fmt.bufPrint(&want, "{s}/a.txt:2:3 open\n", .{dir})) != null); +} + test "a closed file is found again: Recent and /recent list it closed, a look at its row reopens it at its dot, and a jump back does too" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); diff --git a/src/ninep/events.zig b/src/ninep/events.zig index a09f7278..bacf3ad6 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -140,8 +140,9 @@ pub fn announce(p: *Pardes) void { if (!pane.fs.unannounced) continue; pane.fs.unannounced = false; noteLog(p, .new, pane); - // A file opened (a look, a Restore, a boot): first in Recent. - if (pane.file) |f| if (f.output == null and pardes.Pardes.recentKeeps(f.path)) p.recent.opened(p.recentGpa(), f.path); + // A file, PDF or image opened (a look, a Restore, a boot): first in + // Recent. + if (pardes.Pardes.recentPlace(pane)) |d| if (pardes.Pardes.recentKeeps(d.path)) p.recent.opened(p.recentGpa(), d.path); } } diff --git a/src/pardes.zig b/src/pardes.zig index bea79b18..fbc19b8a 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -4709,13 +4709,25 @@ pub const Pardes = struct { return exec.isFile(local); } - /// A file pane closing: its place noted in Recent, when Recent keeps it. + /// A file, PDF or image pane closing: its place noted in Recent, when + /// Recent keeps it. fn noteClosed(p: *Pardes, pane: *const Pane) void { - const d = recentDot(pane) orelse return; + const d = recentPlace(pane) orelse return; if (p.recent.get(d.path) == null and !recentKeeps(d.path)) return; p.recent.closedSel(p.recentGpa(), d.path, d.line, d.col, d.aline, d.acol); } + pub const RecentPlace = struct { path: []const u8, line: u32, col: u32, aline: u32 = 0, acol: u32 = 0 }; + + /// Where a pane is, as Recent keeps it: a file's dot, a PDF's page + /// (`:page:1`), an image's path alone (line 0). Null for any other. + pub fn recentPlace(pane: *const Pane) ?RecentPlace { + if (recentDot(pane)) |d| return .{ .path = d.path, .line = d.line, .col = d.col, .aline = d.aline, .acol = d.acol }; + if (pane.pdfPath()) |path| return .{ .path = path, .line = @intCast(pane.pdfPage().? + 1), .col = 1 }; + if (pane.image) |iv| return .{ .path = iv.path, .line = 0, .col = 0 }; + return null; + } + fn recentDot(pane: *const Pane) ?struct { path: []const u8, line: u32, col: u32, aline: u32 = 0, acol: u32 = 0 } { const f = pane.file orelse return null; if (f.output != null) return null; diff --git a/test/pdf.zig b/test/pdf.zig index 2a18ca3c..fbd580b3 100644 --- a/test/pdf.zig +++ b/test/pdf.zig @@ -384,6 +384,29 @@ test "PdfSections on a PDF with no outline says so and opens nothing" { } } +test "Recent keeps a PDF as it keeps a file: open, then closed at its page" { + if (!pdf_enabled or platform == .web) return; + const gpa = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const fixture = try pdf_impl.makeNoOutlineTestPdf(gpa); + defer gpa.free(fixture); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "plain.pdf", .data = fixture }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)]; + var path_buf: [4200]u8 = undefined; + const path = try std.fmt.bufPrint(&path_buf, "{s}/plain.pdf", .{dir}); + const p = try Pardes.init(gpa, .{ .cols = 80, .rows = 28 }); + defer p.deinit(); + pardes.look.lookAt(p, p.active, path); + p.update(.{ .resize = .{ .cols = 80, .rows = 28 } }); // an update's end announces it + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + try panes.Output.recentText(p, &out.writer, true); + var want: [4300]u8 = undefined; + try std.testing.expect(std.mem.indexOf(u8, out.written(), try std.fmt.bufPrint(&want, "{s}:1:1 open\n", .{path})) != null); +} + test "a look at a PDF's page past its last is a miss, never the last page" { if (!pdf_enabled or platform == .web) return; const gpa = std.testing.allocator; |
