summaryrefslogtreecommitdiff
path: root/src/file_watch.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/file_watch.zig')
-rw-r--r--src/file_watch.zig58
1 files changed, 55 insertions, 3 deletions
diff --git a/src/file_watch.zig b/src/file_watch.zig
index 5d998470..d028488e 100644
--- a/src/file_watch.zig
+++ b/src/file_watch.zig
@@ -60,6 +60,10 @@ pub const Watch = struct {
kq: c_int = -1,
serial: u32,
generation: Generation,
+ /// The path last read while watched (its hash): its going away later is
+ /// a deletion to say, where a path never there (a pane renamed to a new
+ /// name) is not.
+ existed: ?u64 = null,
};
/// The final slot is the one non-pane watched payload: the active ThemeFile.
@@ -221,8 +225,10 @@ fn markDir(fd: c_int, dir_z: [:0]const u8, watches: *const Table) c_int {
switch (builtin.os.tag) {
.linux => {
// CLOSE_WRITE coalesces one writer's writes; MOVED_TO and CREATE
- // cover rename-over and delete-then-recreate saves.
- const mask = linux.IN.CLOSE_WRITE | linux.IN.MOVED_TO | linux.IN.CREATE | linux.IN.ONLYDIR;
+ // cover rename-over and delete-then-recreate saves; DELETE and
+ // MOVED_FROM a file going away, said at once (File.deleted), not
+ // when something else next changes in the directory.
+ const mask = linux.IN.CLOSE_WRITE | linux.IN.MOVED_TO | linux.IN.CREATE | linux.IN.DELETE | linux.IN.MOVED_FROM | linux.IN.ONLYDIR;
return libc.inotify_add_watch(fd, dir_z, mask);
},
.macos => {
@@ -440,7 +446,12 @@ pub fn reloadPane(
}
if (pane.file) |file| {
- const bytes = filesystem.read(core, file.path) catch return false;
+ const bytes = filesystem.read(core, file.path) catch |err| {
+ // Gone from under the pane: said once, the text kept.
+ if (err == error.FileNotFound and watched.existed == std.hash.Wyhash.hash(0, file.path)) pardes.panes.File.deleted(core, @intCast(id));
+ return false;
+ };
+ if (watches[id]) |*live| live.existed = std.hash.Wyhash.hash(0, file.path);
defer core.gpa.free(bytes);
const hash = std.hash.Wyhash.hash(0, bytes);
switch (watched.generation) {
@@ -517,6 +528,7 @@ pub fn applyEffect(
const bytes = filesystem.read(core, file.path) catch return false;
defer core.gpa.free(bytes);
watched.generation = .{ .text = std.hash.Wyhash.hash(0, bytes) };
+ watched.existed = std.hash.Wyhash.hash(0, file.path);
return false;
};
return on and watches[id] != null and reloadPane(core, io, watches, id, false);
@@ -665,6 +677,13 @@ test "restored file watches preserve snapshots and accept later disk changes" {
try tmp.dir.writeFile(io, .{ .sub_path = "watched", .data = "later disk save\n" });
_ = reloadChanged(core, io, gpa, &watches);
+ // A snapshot restored with unsaved text keeps it: a later disk
+ // change is said, never loaded over it (File.changed).
+ if (case.dirty) {
+ try std.testing.expectEqualStrings("restored snapshot\n", pane.file.?.content);
+ try std.testing.expect(pane.file.?.disk_newer != null);
+ continue;
+ }
try std.testing.expectEqualStrings("later disk save\n", pane.file.?.content);
pardes.panes.File.undo(core, pane);
try std.testing.expectEqualStrings("restored snapshot\n", pane.file.?.content);
@@ -821,3 +840,36 @@ test "theme watch reloads valid ZON and keeps the last theme across a bad save"
try std.testing.expect(!reloadTheme(core, gpa, &watches, false));
try std.testing.expectEqual(@as(?[3]u8, .{ 9, 8, 7 }), core.theme().bg);
}
+
+test "a watched file deleted wakes the watch at once, and its pane says so" {
+ if (comptime !supported or !filesystem.platform_has_fs or builtin.os.tag != .linux) return;
+ const io = std.testing.io;
+ const gpa = std.testing.allocator;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(io, .{ .sub_path = "watched", .data = "here\n" });
+ var path_buf: [256]u8 = undefined;
+ const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/watched", .{tmp.sub_path});
+ const core = try pardes.Pardes.init(gpa, .{ .tty_only = true });
+ defer core.deinit();
+ while (core.nextEffect()) |_| {}
+ const id: u8 = @intCast(core.freeSlot().?);
+ const pane = try pardes.panes.File.open(core, id, path, 0);
+ const fd = init(true);
+ if (fd < 0) return error.NoWatcher;
+ defer _ = libc.close(fd);
+ var watches: Table = @splat(null);
+ defer watchPane(fd, &watches, id, null, pane.serial, .{ .text = 0 });
+ while (core.nextEffect()) |effect| switch (effect) {
+ .watch => |watch| if (watch.pane == id and watch.on) {
+ _ = applyEffect(core, io, fd, &watches, id, watch.on, watch.mode);
+ },
+ else => {},
+ };
+ _ = drain(fd);
+ try tmp.dir.deleteFile(io, "watched");
+ var pfd = [_]libc.pollfd{.{ .fd = fd, .events = libc.POLL.IN, .revents = 0 }};
+ try std.testing.expect(libc.poll(&pfd, 1, 1000) > 0);
+ _ = reloadChanged(core, io, gpa, &watches);
+ try std.testing.expect(pane.file.?.disk_gone);
+}