summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/exec.zig10
-rw-r--r--src/ninep/ctl.zig24
-rw-r--r--src/ninep/events.zig2
-rw-r--r--src/ninep/pane.zig2
-rw-r--r--src/pardes.zig19
-rw-r--r--src/recent.zig39
6 files changed, 88 insertions, 8 deletions
diff --git a/src/exec.zig b/src/exec.zig
index 0e9fb6d7..fdb9bdb7 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -395,6 +395,16 @@ pub fn isDirectory(path: []const u8) bool {
return stat.kind == .directory;
}
+/// Whether `path` is a plain file on this host, the turn out while it asks.
+pub fn isFile(path: []const u8) bool {
+ var buf: [4096]u8 = undefined;
+ const z = std.fmt.bufPrintSentinel(&buf, "{s}", .{path}, 0) catch return false;
+ pardes.turn.yield();
+ defer pardes.turn.back();
+ const stat = std.Io.Dir.cwd().statFile(std.Io.Threaded.global_single_threaded.io(), z, .{}) catch return false;
+ return stat.kind == .file;
+}
+
/// With `BootShell replace`, a doc landing in `col` closes a lone pristine
/// shell there, the boot's placeholder, and absorbVWeight hands its space to
/// the doc. With `keep`, the default, the shell stays like any other pane.
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index f4a3d730..dc2d9a84 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -2304,6 +2304,30 @@ test "Back and Forward over a closed file's entries: one reopen is that file's p
try testing.expectEqual(c_serial, p.panes[p.active].?.serial);
}
+test "Recent keeps only files on disk: not a name never saved, nor the served tree's" {
+ 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 = "real.txt", .data = "real\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;
+ const names = [_][]const u8{ try std.fmt.allocPrint(testing.allocator, "{s}/c", .{dir}), try testing.allocator.dupe(u8, "/virtual/zz"), try std.fmt.allocPrint(testing.allocator, "{s}/real.txt", .{dir}) };
+ defer for (names) |n| testing.allocator.free(n);
+ for (names) |name| {
+ const made = try th.newPane(p);
+ _ = wr(p, Node.of(made, .name), try std.fmt.bufPrint(&line, "{s}\n", .{name}));
+ p.sync();
+ _ = wr(p, Node.of(made, .ctl), "Del\n");
+ _ = wr(p, Node.of(made, .ctl), "Del\n");
+ p.sync();
+ }
+ try testing.expect(p.recent.get(names[0]) == null);
+ try testing.expect(p.recent.get(names[1]) == null);
+ try testing.expect(p.recent.get(names[2]) != null);
+}
+
test "/recent shows a name as /index does, a newline in it escaped" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 2fe25071..add0d750 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -141,7 +141,7 @@ pub fn announce(p: *Pardes) void {
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) p.recent.opened(p.recentGpa(), f.path);
+ if (pane.file) |f| if (f.output == null and pardes.Pardes.recentKeeps(f.path)) p.recent.opened(p.recentGpa(), f.path);
}
}
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 0b6f04a4..5ed3f6f4 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -687,7 +687,7 @@ pub fn nameBuffer(p: *Pardes, id: usize, full: []const u8, preserve_tag: bool) !
look.invalidateLookHover(p, id);
p.emit(.{ .watch = .{ .pane = @intCast(id), .on = false } });
events.noteLog(p, .rename, pane);
- if (f.output == null) p.recent.opened(p.recentGpa(), f.path);
+ if (f.output == null and pardes.Pardes.recentKeeps(f.path)) p.recent.opened(p.recentGpa(), f.path);
}
/// Diagnostics go to the +Errors buffer of the pane's directory.
diff --git a/src/pardes.zig b/src/pardes.zig
index 76ec2176..2bf594c4 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4700,6 +4700,21 @@ pub const Pardes = struct {
}
/// A file pane's path and dot, for the recent list (0 when none).
+ /// Whether `path` is one Recent keeps: a file on this host's disk, not
+ /// the served tree's (`/virtual/`) nor a name never saved.
+ pub fn recentKeeps(path: []const u8) bool {
+ const local = filesystem.localPath(path) orelse return false;
+ if (comptime !hosted) return true;
+ return exec.isFile(local);
+ }
+
+ /// A file 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;
+ 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);
+ }
+
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;
@@ -4722,7 +4737,7 @@ pub const Pardes = struct {
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.closedSel(p.recentGpa(), d.path, d.line, d.col, d.aline, d.acol);
+ for (p.panes) |slot| if (slot) |pane| p.noteClosed(pane);
p.saveRecent();
p.recent.deinit(p.recentGpa());
look.cancelLookHover(p);
@@ -4835,7 +4850,7 @@ 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.closedSel(p.recentGpa(), d.path, d.line, d.col, d.aline, d.acol);
+ p.noteClosed(pane);
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 } });
diff --git a/src/recent.zig b/src/recent.zig
index 9f18d737..91c0afeb 100644
--- a/src/recent.zig
+++ b/src/recent.zig
@@ -18,6 +18,10 @@ pub const Entry = struct {
/// The selection's other end then (its anchor), 0 when there was none.
aline: u32 = 0,
acol: u32 = 0,
+ /// Open in a pane now (opened, not closed since): never the one the
+ /// cap drops. ponytail: a pane renamed away leaves its old path marked
+ /// open until the session ends; kept rather than tracked by pane.
+ open: bool = false,
};
pub const List = struct {
@@ -41,9 +45,14 @@ pub const List = struct {
var entry: Entry = if (l.find(path)) |i| l.entries.orderedRemove(i) else .{
.path = gpa.dupe(u8, path) catch return,
};
+ entry.open = true;
+ // The oldest closed file goes for it; an open one never does.
if (l.entries.items.len >= cap) {
- const last = l.entries.pop().?;
- gpa.free(last.path);
+ var i = l.entries.items.len;
+ while (i > 0) : (i -= 1) if (!l.entries.items[i - 1].open) {
+ gpa.free(l.entries.orderedRemove(i - 1).path);
+ break;
+ };
}
l.entries.insert(gpa, 0, entry) catch {
gpa.free(entry.path);
@@ -70,6 +79,7 @@ pub const List = struct {
l.entries.items[i].col = col;
l.entries.items[i].aline = aline;
l.entries.items[i].acol = acol;
+ l.entries.items[i].open = false;
l.dirty = true;
}
@@ -80,7 +90,9 @@ pub const List = struct {
/// One line a file, `<line> <col> <aline> <acol> <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} {d} {d} {s}\n", .{ e.line, e.col, e.aline, e.acol, e.path });
+ // A name with a newline in it would read back as two lines: not kept.
+ for (l.entries.items) |e| if (std.mem.indexOfScalar(u8, e.path, '\n') == null)
+ 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.
@@ -135,7 +147,26 @@ test "the list keeps a path once, most recent first, its dot from closing, cappe
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);
+ const path = std.fmt.bufPrint(&name, "/f{d}", .{i}) catch unreachable;
+ l.opened(gpa, path);
+ l.closed(gpa, path, 1, 1);
}
try std.testing.expectEqual(@as(usize, cap), l.entries.items.len);
}
+
+test "the cap drops the oldest closed file, never an open one" {
+ const gpa = std.testing.allocator;
+ var l: List = .{};
+ defer l.deinit(gpa);
+ var name: [16]u8 = undefined;
+ // The oldest is open; the next is closed.
+ l.opened(gpa, "/open");
+ l.opened(gpa, "/shut");
+ l.closed(gpa, "/shut", 1, 1);
+ for (2..cap) |i| l.opened(gpa, std.fmt.bufPrint(&name, "/f{d}", .{i}) catch unreachable);
+ try std.testing.expectEqual(@as(usize, cap), l.entries.items.len);
+ l.opened(gpa, "/new");
+ try std.testing.expectEqual(@as(usize, cap), l.entries.items.len);
+ try std.testing.expect(l.get("/open") != null);
+ try std.testing.expect(l.get("/shut") == null);
+}