From 81ec44ca3b103bdc755e9f5d73a270c05c88617b Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 06:01:01 -0300 Subject: A watched file's deletion wakes the watch at once The directory mark took CLOSE_WRITE, MOVED_TO and CREATE only, so a file deleted or moved away was noticed only when something else in the directory next changed. The mark takes DELETE and MOVED_FROM too, so changed deleted comes at once; a test deletes a watched file and finds the watch readable within a second and the pane told. Co-Authored-By: Claude Opus 5.5 --- src/file_watch.zig | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/file_watch.zig b/src/file_watch.zig index 71a4680d..d028488e 100644 --- a/src/file_watch.zig +++ b/src/file_watch.zig @@ -225,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 => { @@ -838,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); +} -- cgit v1.3