From 0dc78afb1cc6eb21bd6f73ff7d2a5b5e2a8a1563 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 31 Mar 2026 21:35:56 +0200 Subject: [PATCH] fix(fsevents): emit subtree created events on directory move-in --- src/backend/FSEvents.zig | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/backend/FSEvents.zig b/src/backend/FSEvents.zig index deab2f3..f4b6e60 100644 --- a/src/backend/FSEvents.zig +++ b/src/backend/FSEvents.zig @@ -9,7 +9,7 @@ pub const detects_file_modifications = true; pub const emits_close_events = false; pub const emits_rename_for_files = false; pub const emits_rename_for_dirs = false; -pub const emits_subtree_created_on_movein = false; // FSEvents emits per-path events only; no subtree synthesis +pub const emits_subtree_created_on_movein = true; handler: *Handler, stream: ?*anyopaque, // FSEventStreamRef @@ -251,6 +251,10 @@ fn callback( // so move-in appears as created and move-out as deleted on all platforms. const exists = if (std.fs.accessAbsolute(path, .{})) |_| true else |_| false; ctx.handler.change(path, if (exists) .created else .deleted, ot) catch {}; + // If a directory was moved in from outside the watched tree, FSEvents + // only fires for the directory itself - not for its pre-existing contents. + // Walk the subtree and emit individual created events for each entry. + if (exists and ot == .dir) emit_subtree_created(ctx.handler, path); // If a write was coalesced with a move-in, also emit the modify. if (exists and flags & kFSEventStreamEventFlagItemModified != 0) { ctx.handler.change(path, .modified, ot) catch {}; @@ -263,6 +267,27 @@ fn callback( } } +// Walk dir_path recursively, emitting 'created' events for all contents. +// Called when a directory is moved into the watched tree: FSEvents fires +// ItemRenamed only for the directory itself, not for its pre-existing contents. +// Uses only stack storage so it can be called from the GCD callback thread. +fn emit_subtree_created(handler: *Handler, dir_path: []const u8) void { + var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; + defer dir.close(); + var iter = dir.iterate(); + while (iter.next() catch return) |entry| { + const ot: ObjectType = switch (entry.kind) { + .file => .file, + .directory => .dir, + else => continue, + }; + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, entry.name }) catch continue; + handler.change(full_path, .created, ot) catch {}; + if (ot == .dir) emit_subtree_created(handler, full_path); + } +} + pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) error{ WatchFailed, OutOfMemory }!void { if (self.watches.contains(path)) return; const owned = try allocator.dupe(u8, path);