diff options
Diffstat (limited to 'src/file_watch.zig')
| -rw-r--r-- | src/file_watch.zig | 58 |
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); +} |
