Rewrite MacOS directory watcher. (#2268)

diff --git a/pkgs/watcher/CHANGELOG.md b/pkgs/watcher/CHANGELOG.md
index fa92f0e..18018c2 100644
--- a/pkgs/watcher/CHANGELOG.md
+++ b/pkgs/watcher/CHANGELOG.md
@@ -37,8 +37,9 @@
   issues: tracking failure following subdirectory move, incorrect events when
   there are changes in a recently-moved subdirectory, incorrect events due to
   various situations involving subdirectory moves.
-- Bug fix: with `DirectoryWatcher` on MacOS, fix events for changes in new
-  directories: don't emit duplicate ADD, don't emit MODIFY without ADD.
+- Bug fix: new `DirectoryWatcher` implementation on MacOS that fixes various
+  issues including duplicate events for changes in new directories, incorrect
+  events when close together directory renames have overlapping names.
 - Bug fix: with `FileWatcher` on MacOS, a modify event was sometimes reported if
   the file was created immediately before the watcher was created. Now, if the
   file exists when the watcher is created then this modify event is not sent.
diff --git a/pkgs/watcher/lib/src/directory_watcher.dart b/pkgs/watcher/lib/src/directory_watcher.dart
index a600acb..0b2197e 100644
--- a/pkgs/watcher/lib/src/directory_watcher.dart
+++ b/pkgs/watcher/lib/src/directory_watcher.dart
@@ -7,7 +7,7 @@
 import '../watcher.dart';
 import 'custom_watcher_factory.dart';
 import 'directory_watcher/linux.dart';
-import 'directory_watcher/mac_os.dart';
+import 'directory_watcher/macos.dart';
 import 'directory_watcher/windows_resubscribable_watcher.dart';
 
 /// Watches the contents of a directory and emits [WatchEvent]s when something
@@ -54,7 +54,7 @@
       );
       if (customWatcher != null) return customWatcher;
       if (Platform.isLinux) return LinuxDirectoryWatcher(directory);
-      if (Platform.isMacOS) return MacOSDirectoryWatcher(directory);
+      if (Platform.isMacOS) return MacosDirectoryWatcher(directory);
       if (Platform.isWindows) {
         return WindowsDirectoryWatcher(directory,
             runInIsolate: runInIsolateOnWindows);
diff --git a/pkgs/watcher/lib/src/directory_watcher/linux/native_watch.dart b/pkgs/watcher/lib/src/directory_watcher/linux/native_watch.dart
index f569795..0908463 100644
--- a/pkgs/watcher/lib/src/directory_watcher/linux/native_watch.dart
+++ b/pkgs/watcher/lib/src/directory_watcher/linux/native_watch.dart
@@ -6,8 +6,8 @@
 import 'dart:io';
 
 import '../../event.dart';
+import '../../unix_paths.dart';
 import '../../utils.dart';
-import 'paths.dart';
 
 /// Watches a directory with the native Linux watcher.
 ///
@@ -104,8 +104,7 @@
         // watched directory was deleted. But, it might be an incorrect event
         // that is the deletion event for the old location in a move. So, check
         // if the directory is actually now missing.
-        if (watchedDirectory.statSync().type ==
-            FileSystemEntityType.directory) {
+        if (watchedDirectory.typeSync() == FileSystemEntityType.directory) {
           // The directory is still present, indicating either a watch failure
           // or that the directoy has been replaced with a new one. Either way,
           // restart watching.
diff --git a/pkgs/watcher/lib/src/directory_watcher/linux/paths.dart b/pkgs/watcher/lib/src/directory_watcher/linux/paths.dart
deleted file mode 100644
index 311542b..0000000
--- a/pkgs/watcher/lib/src/directory_watcher/linux/paths.dart
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'dart:io';
-
-import 'package:path/path.dart' as p;
-
-import '../../event.dart';
-import '../../watch_event.dart';
-
-/// An absolute file path.
-extension type AbsolutePath(String _string) {
-  /// Whether this immediate parent directory of this path is [directory].
-  bool isIn(AbsolutePath directory) => p.dirname(_string) == directory._string;
-
-  /// This path relative to [root].
-  ///
-  /// Returns the empty string if this path is [root].
-  ///
-  /// Otherwise, throws if this path does not start with [root].
-  RelativePath relativeTo(AbsolutePath root) {
-    if (!_string.startsWith(root._string)) throw ArgumentError(root);
-    if (_string == root._string) return RelativePath('');
-    return RelativePath(_string.substring(root._string.length + 1));
-  }
-
-  /// The last path segment of this path.
-  RelativePath get basename => RelativePath(p.basename(_string));
-
-  /// Lists the directory at this path, ignoring symlinks.
-  List<FileSystemEntity> listSync() =>
-      Directory(_string).listSync(followLinks: false);
-
-  /// Watches the directory at this path.
-  Stream<FileSystemEvent> watch() => Directory(_string).watch();
-
-  /// Gets the [FileStat] for this path.
-  FileStat statSync() => FileStat.statSync(_string);
-
-  /// Returns this path followed by [path].
-  AbsolutePath append(RelativePath path) =>
-      AbsolutePath('$_string/${path._string}');
-
-  /// Add event for this path.
-  WatchEvent get addEvent => WatchEvent(ChangeType.ADD, _string);
-
-  /// Modify event for this path.
-  WatchEvent get modifyEvent => WatchEvent(ChangeType.MODIFY, _string);
-
-  /// Remove event for this path.
-  WatchEvent get removeEvent => WatchEvent(ChangeType.REMOVE, _string);
-}
-
-extension FileSystemEntityExtensions on FileSystemEntity {
-  /// The event path relative to [root].
-  ///
-  /// Throws if not under [root].
-  RelativePath pathRelativeTo(AbsolutePath root) =>
-      AbsolutePath(path).relativeTo(root);
-}
-
-extension EventExtensions on Event {
-  /// The event [path] as an [AbsolutePath].
-  AbsolutePath get absolutePath => AbsolutePath(path);
-
-  /// The event [path] relative to [root].
-  RelativePath pathRelativeTo(AbsolutePath root) =>
-      AbsolutePath(path).relativeTo(root);
-
-  /// Whether the event path parent directory is exactly [directory].
-  bool isIn(AbsolutePath directory) => AbsolutePath(path).isIn(directory);
-}
-
-/// A relative file path.
-extension type RelativePath(String _string) {}
diff --git a/pkgs/watcher/lib/src/directory_watcher/linux/watch_tree.dart b/pkgs/watcher/lib/src/directory_watcher/linux/watch_tree.dart
index 763b312..81ef863 100644
--- a/pkgs/watcher/lib/src/directory_watcher/linux/watch_tree.dart
+++ b/pkgs/watcher/lib/src/directory_watcher/linux/watch_tree.dart
@@ -5,10 +5,10 @@
 import 'dart:io';
 
 import '../../event.dart';
+import '../../unix_paths.dart';
 import '../../utils.dart';
 import '../../watch_event.dart';
 import 'native_watch.dart';
-import 'paths.dart';
 
 /// Linux directory watcher.
 ///
@@ -148,9 +148,9 @@
   /// needs to be watched again.
   void _poll(RelativePath path) {
     logForTesting?.call('Watch,$watchedDirectory,_poll,$path');
-    final stat = watchedDirectory.append(path).statSync();
-    if (stat.type == FileSystemEntityType.file ||
-        stat.type == FileSystemEntityType.link) {
+    final type = watchedDirectory.append(path).typeSync();
+    if (type == FileSystemEntityType.file ||
+        type == FileSystemEntityType.link) {
       logForTesting?.call('Watch,$watchedDirectory,poll,$path,file');
       if (_directories.containsKey(path)) {
         _emitDeleteDirectory(path);
@@ -160,7 +160,7 @@
       } else {
         _addFile(path);
       }
-    } else if (stat.type == FileSystemEntityType.directory) {
+    } else if (type == FileSystemEntityType.directory) {
       logForTesting?.call('Watch,$watchedDirectory,poll,$path,directory');
       if (_files.contains(path)) {
         _emitDeleteFile(path);
diff --git a/pkgs/watcher/lib/src/directory_watcher/linux/watch_tree_root.dart b/pkgs/watcher/lib/src/directory_watcher/linux/watch_tree_root.dart
index a52e19a..624216d 100644
--- a/pkgs/watcher/lib/src/directory_watcher/linux/watch_tree_root.dart
+++ b/pkgs/watcher/lib/src/directory_watcher/linux/watch_tree_root.dart
@@ -4,9 +4,9 @@
 
 import 'dart:async';
 
+import '../../unix_paths.dart';
 import '../../utils.dart';
 import '../../watch_event.dart';
-import 'paths.dart';
 import 'watch_tree.dart';
 
 /// Linux directory watcher using a [WatchTree].
diff --git a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart
deleted file mode 100644
index 9328997..0000000
--- a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart
+++ /dev/null
@@ -1,375 +0,0 @@
-// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'dart:async';
-import 'dart:io';
-
-import 'package:path/path.dart' as p;
-
-import '../directory_watcher.dart';
-import '../event.dart';
-import '../path_set.dart';
-import '../resubscribable.dart';
-import '../utils.dart';
-import '../watch_event.dart';
-import 'directory_list.dart';
-
-/// Uses the FSEvents subsystem to watch for filesystem events.
-///
-/// FSEvents has two main idiosyncrasies that this class works around. First, it
-/// will occasionally report events that occurred before the filesystem watch
-/// was initiated. Second, if multiple events happen to the same file in close
-/// succession, it won't report them in the order they occurred. See issue
-/// 14373.
-///
-/// This also works around issues 16003 and 14849 in the implementation of
-/// [Directory.watch].
-class MacOSDirectoryWatcher extends ResubscribableWatcher
-    implements DirectoryWatcher {
-  @override
-  String get directory => path;
-
-  MacOSDirectoryWatcher(String directory)
-      : super(directory, () => _MacOSDirectoryWatcher(directory));
-}
-
-class _MacOSDirectoryWatcher
-    implements DirectoryWatcher, ManuallyClosedWatcher {
-  @override
-  String get directory => path;
-  @override
-  final String path;
-
-  @override
-  Stream<WatchEvent> get events => _eventsController.stream;
-  final _eventsController = StreamController<WatchEvent>.broadcast();
-
-  @override
-  bool get isReady => _readyCompleter.isCompleted;
-
-  @override
-  Future<void> get ready => _readyCompleter.future;
-  final _readyCompleter = Completer<void>();
-
-  /// The set of files that are known to exist recursively within the watched
-  /// directory.
-  ///
-  /// The state of files on the filesystem is compared against this to determine
-  /// the real change that occurred when working around issue 14373. This is
-  /// also used to emit REMOVE events when subdirectories are moved out of the
-  /// watched directory.
-  final PathSet _files;
-
-  /// The subscription to the stream returned by [Directory.watch].
-  ///
-  /// This is separate from [_listSubscriptions] because this stream
-  /// occasionally needs to be resubscribed in order to work around issue 14849.
-  StreamSubscription<List<Event>>? _watchSubscription;
-
-  /// The subscription to the [Directory.list] call for the initial listing of
-  /// the directory to determine its initial state.
-  StreamSubscription<FileSystemEntity>? _initialListSubscription;
-
-  /// The subscriptions to [Directory.list] calls for listing the contents of a
-  /// subdirectory that was moved into the watched directory.
-  final _listSubscriptions = <StreamSubscription<FileSystemEntity>>{};
-
-  /// The timer for tracking how long we wait for an initial batch of bogus
-  /// events (see issue 14373).
-  late Timer _bogusEventTimer;
-
-  _MacOSDirectoryWatcher(this.path) : _files = PathSet(path) {
-    _startWatch();
-
-    // Before we're ready to emit events, wait for [_listDir] to complete and
-    // for enough time to elapse that if bogus events (issue 14373) would be
-    // emitted, they will be.
-    //
-    // If we do receive a batch of events, [_onBatch] will ensure that these
-    // futures don't fire and that the directory is re-listed.
-    Future.wait([_listDir(), _waitForBogusEvents()]).then((_) {
-      if (!isReady) {
-        _readyCompleter.complete();
-      }
-    });
-  }
-
-  @override
-  void close() {
-    _watchSubscription?.cancel();
-    _initialListSubscription?.cancel();
-    _watchSubscription = null;
-    _initialListSubscription = null;
-
-    for (var subscription in _listSubscriptions) {
-      subscription.cancel();
-    }
-    _listSubscriptions.clear();
-
-    _eventsController.close();
-  }
-
-  /// The callback that's run when [Directory.watch] emits a batch of events.
-  void _onBatch(List<Event> batch) {
-    logForTesting?.call('onBatch: $batch');
-
-    // If we get a batch of events before we're ready to begin emitting events,
-    // it's probable that it's a batch of pre-watcher events (see issue 14373).
-    // Ignore those events and re-list the directory.
-    if (!isReady) {
-      // Cancel the timer because bogus events only occur in the first batch, so
-      // we can fire [ready] as soon as we're done listing the directory.
-      _bogusEventTimer.cancel();
-      _listDir().then((_) {
-        if (!isReady) {
-          _readyCompleter.complete();
-        }
-      });
-      return;
-    }
-
-    _sortEvents(batch).forEach((path, eventSet) {
-      var canonicalEvent = _canonicalEvent(eventSet);
-      var events = canonicalEvent == null
-          ? _eventsBasedOnFileSystem(path)
-          : [canonicalEvent];
-
-      for (var event in events) {
-        switch (event.type) {
-          case EventType.createFile:
-          case EventType.modifyFile:
-            // The type can be incorrect due to a race with listing a new
-            // directory or due to a file being copied over an existing one.
-            // Choose the type to emit based on the previous emitted state.
-            var type =
-                _files.contains(path) ? ChangeType.MODIFY : ChangeType.ADD;
-
-            _emitEvent(type, path);
-            _files.add(path);
-
-          case EventType.createDirectory:
-            if (_files.containsDir(path)) continue;
-
-            var stream = Directory(path)
-                .listRecursivelyIgnoringErrors(followLinks: false);
-            var subscription = stream.listen((entity) {
-              if (entity is Directory) return;
-              if (_files.contains(entity.path)) return;
-
-              _emitEvent(ChangeType.ADD, entity.path);
-              _files.add(entity.path);
-            }, cancelOnError: true);
-            subscription.onDone(() {
-              _listSubscriptions.remove(subscription);
-            });
-            subscription.onError(_emitError);
-            _listSubscriptions.add(subscription);
-
-          case EventType.delete:
-            for (var removedPath in _files.remove(path)) {
-              _emitEvent(ChangeType.REMOVE, removedPath);
-            }
-
-          // Dropped by [Event.checkAndConvert].
-          case EventType.moveFile:
-          case EventType.moveDirectory:
-          case EventType.modifyDirectory:
-            assert(event.type.isNeverReceivedOnMacOS);
-        }
-      }
-    });
-  }
-
-  /// Sort all the events in a batch into sets based on their path.
-  ///
-  /// Events for [path] are discarded.
-  ///
-  /// Events under directories that are created are discarded.
-  Map<String, Set<Event>> _sortEvents(List<Event> batch) {
-    var eventsForPaths = <String, Set<Event>>{};
-
-    // FSEvents can report past events, including events on the root directory
-    // such as it being created. We want to ignore these. If the directory is
-    // really deleted, that's handled by [_onDone].
-    batch = batch.where((event) => event.path != path).toList();
-
-    // Events within directories that already have create events are not needed
-    // as the directory's full content will be listed.
-    var createdDirectories = unionAll(batch.map((event) {
-      return event.type == EventType.createDirectory
-          ? {event.path}
-          : const <String>{};
-    }));
-
-    bool isInCreatedDirectory(String path) =>
-        createdDirectories.any((dir) => path != dir && p.isWithin(dir, path));
-
-    void addEvent(String path, Event event) {
-      if (isInCreatedDirectory(path)) return;
-      eventsForPaths.putIfAbsent(path, () => <Event>{}).add(event);
-    }
-
-    for (var event in batch) {
-      addEvent(event.path, event);
-    }
-
-    return eventsForPaths;
-  }
-
-  /// Returns the canonical event from a batch of events on the same path, or
-  /// `null` to indicate that the filesystem should be checked.
-  Event? _canonicalEvent(Set<Event> batch) {
-    // If the batch is empty, return `null`.
-    if (batch.isEmpty) return null;
-
-    // Resolve the event type for the batch.
-    var types = batch.map((e) => e.type).toSet();
-    EventType type;
-    if (types.length == 1) {
-      // There's only one event.
-      type = types.single;
-    } else if (types.length == 2 &&
-        types.contains(EventType.modifyFile) &&
-        types.contains(EventType.createFile)) {
-      // Combine events of type [EventType.modifyFile] and
-      // [EventType.createFile] to one event.
-      if (_files.contains(batch.first.path)) {
-        // The file already existed: this can happen due to a create from
-        // before the watcher started being reported.
-        type = EventType.modifyFile;
-      } else {
-        type = EventType.createFile;
-      }
-    } else {
-      // There are incompatible event types, check the filesystem.
-      return null;
-    }
-
-    // Issue 16003 means that a CREATE event for a directory can indicate
-    // that the directory was moved and then re-created.
-    // [_eventsBasedOnFileSystem] will handle this correctly by producing a
-    // DELETE event followed by a CREATE event if the directory exists.
-    if (type == EventType.createDirectory) {
-      return null;
-    }
-
-    return batch.firstWhere((e) => e.type == type);
-  }
-
-  /// Returns one or more events that describe the change between the last known
-  /// state of [path] and its current state on the filesystem.
-  ///
-  /// This returns a list whose order should be reflected in the events emitted
-  /// to the user, unlike the batched events from [Directory.watch]. The
-  /// returned list may be empty, indicating that no changes occurred to [path]
-  /// (probably indicating that it was created and then immediately deleted).
-  List<Event> _eventsBasedOnFileSystem(String path) {
-    var fileExisted = _files.contains(path);
-    var dirExisted = _files.containsDir(path);
-    var fileExists = File(path).existsSync();
-    var dirExists = Directory(path).existsSync();
-
-    var events = <Event>[];
-    if (fileExisted) {
-      if (fileExists) {
-        events.add(Event.modifyFile(path));
-      } else {
-        events.add(Event.delete(path));
-      }
-    } else if (dirExisted) {
-      if (dirExists) {
-        // If we got contradictory events for a directory that used to exist and
-        // still exists, we need to rescan the whole thing in case it was
-        // replaced with a different directory.
-        events.add(Event.delete(path));
-        events.add(Event.createDirectory(path));
-      } else {
-        events.add(Event.delete(path));
-      }
-    }
-
-    if (!fileExisted && fileExists) {
-      events.add(Event.createFile(path));
-    } else if (!dirExisted && dirExists) {
-      events.add(Event.createDirectory(path));
-    }
-
-    return events;
-  }
-
-  /// The callback that's run when the [Directory.watch] stream is closed.
-  void _onDone() {
-    _watchSubscription = null;
-
-    // If the directory still exists and we're still expecting bogus events,
-    // this is probably issue 14849 rather than a real close event. We should
-    // just restart the watcher.
-    if (!isReady && Directory(path).existsSync()) {
-      _startWatch();
-      return;
-    }
-
-    // FSEvents can fail to report the contents of the directory being removed
-    // when the directory itself is removed, so we need to manually mark the
-    // files as removed.
-    for (var file in _files.paths) {
-      _emitEvent(ChangeType.REMOVE, file);
-    }
-    _files.clear();
-    close();
-  }
-
-  /// Start or restart the underlying [Directory.watch] stream.
-  void _startWatch() {
-    // Batch the FSEvent changes together so that we can dedup events.
-    var innerStream =
-        Directory(path).watch(recursive: true).batchAndConvertEvents();
-    _watchSubscription = innerStream.listen(_onBatch,
-        onError: _eventsController.addError, onDone: _onDone);
-  }
-
-  /// Starts or restarts listing the watched directory to get an initial picture
-  /// of its state.
-  Future<void> _listDir() {
-    assert(!isReady);
-    _initialListSubscription?.cancel();
-
-    _files.clear();
-    var completer = Completer<void>();
-    var stream =
-        Directory(path).listRecursivelyIgnoringErrors(followLinks: false);
-    _initialListSubscription = stream.listen((entity) {
-      if (entity is! Directory) _files.add(entity.path);
-    }, onError: _emitError, onDone: completer.complete, cancelOnError: true);
-    return completer.future;
-  }
-
-  /// Wait 200ms for a batch of bogus events (issue 14373) to come in.
-  ///
-  /// 200ms is short in terms of human interaction, but longer than any Mac OS
-  /// watcher tests take on the bots, so it should be safe to assume that any
-  /// bogus events will be signaled in that time frame.
-  Future<void> _waitForBogusEvents() {
-    var completer = Completer<void>();
-    _bogusEventTimer =
-        Timer(const Duration(milliseconds: 200), completer.complete);
-    return completer.future;
-  }
-
-  /// Emit an event with the given [type] and [path].
-  void _emitEvent(ChangeType type, String path) {
-    if (!isReady) return;
-    _eventsController.add(WatchEvent(type, path));
-  }
-
-  /// Emit an error, then close the watcher.
-  void _emitError(Object error, StackTrace stackTrace) {
-    // Guarantee that ready always completes.
-    if (!isReady) {
-      _readyCompleter.complete();
-    }
-    _eventsController.addError(error, stackTrace);
-    close();
-  }
-}
diff --git a/pkgs/watcher/lib/src/directory_watcher/macos.dart b/pkgs/watcher/lib/src/directory_watcher/macos.dart
new file mode 100644
index 0000000..0cfceb2
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/macos.dart
@@ -0,0 +1,53 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import '../directory_watcher.dart';
+import '../resubscribable.dart';
+import '../watch_event.dart';
+import 'macos/watched_directory_tree.dart';
+
+/// Resubscribable MacOS directory watcher that watches using
+/// [_MacosDirectoryWatcher].
+class MacosDirectoryWatcher extends ResubscribableWatcher
+    implements DirectoryWatcher {
+  @override
+  String get directory => path;
+
+  MacosDirectoryWatcher(String directory)
+      : super(directory, () => _MacosDirectoryWatcher(directory));
+}
+
+/// Macos directory watcher that watches using [WatchedDirectoryTree].
+class _MacosDirectoryWatcher
+    implements DirectoryWatcher, ManuallyClosedWatcher {
+  @override
+  final String path;
+  @override
+  String get directory => path;
+
+  @override
+  Stream<WatchEvent> get events => _eventsController.stream;
+  final _eventsController = StreamController<WatchEvent>();
+
+  @override
+  bool get isReady => _readyCompleter.isCompleted;
+
+  @override
+  Future<void> get ready => _readyCompleter.future;
+  final _readyCompleter = Completer<void>();
+
+  late final WatchedDirectoryTree _watchTree;
+
+  _MacosDirectoryWatcher(this.path) {
+    _watchTree = WatchedDirectoryTree(
+        watchedDirectory: path,
+        eventsController: _eventsController,
+        readyCompleter: _readyCompleter);
+  }
+
+  @override
+  void close() => _watchTree.stopWatching();
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/macos/directory_tree.dart b/pkgs/watcher/lib/src/directory_watcher/macos/directory_tree.dart
new file mode 100644
index 0000000..2cac86a
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/macos/directory_tree.dart
@@ -0,0 +1,224 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import '../../unix_paths.dart';
+import '../../utils.dart';
+import '../../watch_event.dart';
+import 'event_tree.dart';
+
+/// MacOS directory tree.
+///
+/// Tracks state for a single directory and maintains child [DirectoryTree]
+/// instances for subdirectories.
+class DirectoryTree {
+  final AbsolutePath watchedDirectory;
+
+  /// Known subdirectories and their directory trees.
+  final Map<PathSegment, DirectoryTree> _directories = {};
+
+  /// Known files.
+  final Set<PathSegment> _files = {};
+
+  /// Called to emit a user-visible watch event.
+  final void Function(WatchEvent) _emitEvent;
+
+  /// Watches [watchedDirectory] and its subdirectories.
+  ///
+  /// Pass the handler [emitEvent].
+  DirectoryTree({
+    required this.watchedDirectory,
+    required void Function(WatchEvent) emitEvent,
+  }) : _emitEvent = emitEvent {
+    logForTesting?.call('DirectoryTree(),$watchedDirectory');
+    poll(EventTree.singleEvent());
+  }
+
+  /// Polls [watchedDirectory].
+  ///
+  /// Directories and files mentioned in [eventTree] are polled. This includes
+  /// calling [poll] on subdirectories with subtrees of [eventTree] and fully
+  /// polling newly-discovered or recreated directories.
+  void poll(EventTree eventTree) {
+    logForTesting?.call('DirectoryTree,$watchedDirectory,poll,$eventTree');
+
+    // If there's an event mentioning this directory then the whole directory
+    // needs polling.
+    if (eventTree.isSingleEvent) {
+      _pollDirectory();
+      return;
+    }
+
+    // Poll filesystem entities in this directory, call `poll` on subdirectories
+    // with subtrees of [eventTree].
+    for (final entry in eventTree.entries) {
+      if (entry.value.isSingleEvent) {
+        _pollPathSegment(entry.key);
+      } else {
+        final directory = _directories[entry.key];
+        if (directory != null) {
+          directory.poll(entry.value);
+        } else {
+          // Events for a directory but it's not a known directory.
+          // Call `_pollPathSegment` which will do the right thing based on
+          // whether it currently exists and whether it's a file or a directory.
+          _pollPathSegment(entry.key);
+        }
+      }
+    }
+  }
+
+  /// Polls the directory.
+  ///
+  /// Emits "add" events for newly-discovered files, "modify" events for known
+  /// files that are still present, and "delete" events for known files that no
+  /// longer exist.
+  ///
+  /// "modify" events are emitted for known files because a directory poll
+  /// happens due to the directory being deleted and/or created; a known file
+  /// might be different because the whole directory was recreated.
+  ///
+  /// Starts tracking newly-discovered directories. Polls known directories that
+  /// are still present. For known directories that no longer exist, emits
+  /// "delete" events and stops tracking them.
+  void _pollDirectory() {
+    logForTesting?.call('DirectoryTree,$watchedDirectory,_pollDirectory');
+
+    final listedFiles = <PathSegment>{};
+    final listedDirectories = <PathSegment>{};
+
+    try {
+      for (final entity in watchedDirectory.listSync()) {
+        if (entity is File || entity is Link) {
+          listedFiles.add(entity.pathSegmentRelativeTo(watchedDirectory));
+        } else if (entity is Directory) {
+          listedDirectories.add(entity.pathSegmentRelativeTo(watchedDirectory));
+        }
+      }
+    } catch (_) {
+      // Nothing found, use empty sets so everything is handled as deleted.
+    }
+
+    logForTesting?.call('Watch,$watchedDirectory,list,'
+        'files=$listedFiles,directories=$listedDirectories');
+    // Emit deletes for missing files.
+    for (final file in _files.toList()) {
+      if (!listedFiles.contains(file)) {
+        _emitDeleteFile(file);
+      }
+    }
+    // Emit for present files.
+    for (final file in listedFiles) {
+      if (_files.contains(file)) {
+        _emitModifyFile(file);
+      } else {
+        _addFile(file);
+      }
+    }
+
+    // Emit deletes for missing directories.
+    for (final directory in _directories.keys.toList()) {
+      if (!listedDirectories.contains(directory)) {
+        _emitDeleteDirectory(directory);
+      }
+    }
+    // Handle present directories.
+    for (final directory in listedDirectories) {
+      _trackOrPollDirectory(directory);
+    }
+  }
+
+  /// Polls a file or directory directoly under [watchedDirectory].
+  ///
+  /// If it's now a directory, tracks it, or polls it if it's already a known
+  /// directory. If it's not now a directory and it was before, emits deletes
+  /// for the directory and stops tracking it.
+  ///
+  /// If it's now a file (or a link), emits "add" if it's new or "modify" if it
+  /// was a known file. If it's not now a file and it was before, emits a
+  /// delete.
+  void _pollPathSegment(PathSegment segment) {
+    logForTesting
+        ?.call('DirectoryTree,$watchedDirectory,_pollPathSegment,$segment');
+
+    final type = watchedDirectory.append(segment).typeSync();
+
+    if (type == FileSystemEntityType.directory) {
+      _trackOrPollDirectory(segment);
+    } else {
+      if (_directories.containsKey(segment)) {
+        _emitDeleteDirectory(segment);
+      }
+    }
+
+    if (type == FileSystemEntityType.file ||
+        type == FileSystemEntityType.link) {
+      if (_files.contains(segment)) {
+        _emitModifyFile(segment);
+      } else {
+        _addFile(segment);
+      }
+    } else {
+      if (_files.remove(segment)) {
+        _emitDeleteFile(segment);
+      }
+    }
+  }
+
+  /// Emits events for deleting the entire tree.
+  void emitDeleteTree() {
+    logForTesting?.call('DirectoryTree,$watchedDirectory,_emitDeleteTree');
+    for (final file in _files.toList()) {
+      _emitDeleteFile(file);
+    }
+    for (final directory in _directories.keys.toList()) {
+      _emitDeleteDirectory(directory);
+    }
+  }
+
+  /// Tracks or polls [directory].
+  ///
+  /// If [directory] is known, polls it. If not, starts tracking it, emitting
+  /// "add" events for discovered files.
+  void _trackOrPollDirectory(PathSegment directory) {
+    logForTesting
+        ?.call('Watch,$watchedDirectory,_trackOrPollDirectory,$directory');
+    if (_directories.containsKey(directory)) {
+      // Poll known directories.
+      _directories[directory]!.poll(EventTree.singleEvent());
+    } else {
+      /// Start tracking new directories.
+      _directories[directory] = DirectoryTree(
+          emitEvent: _emitEvent,
+          watchedDirectory: watchedDirectory.append(directory));
+    }
+  }
+
+  /// Adds [file] to known [_files].
+  void _addFile(PathSegment file) {
+    logForTesting?.call('Watch,$watchedDirectory,_addFile,$file');
+    _files.add(file);
+    _emitEvent(watchedDirectory.append(file).addEvent);
+  }
+
+  /// Emits a "modify" event for [file].
+  void _emitModifyFile(PathSegment file) {
+    logForTesting?.call('Watch,$watchedDirectory,_emitModifyFile,$file');
+    _emitEvent(watchedDirectory.append(file).modifyEvent);
+  }
+
+  /// Emits a "delete" event for [file] and removes it from [_files].
+  void _emitDeleteFile(RelativePath file) {
+    logForTesting?.call('Watch,$watchedDirectory,deleteFile,$file');
+    _files.remove(file);
+    _emitEvent(watchedDirectory.append(file).removeEvent);
+  }
+
+  /// Stops watching [directory] and removes it from [_directories].
+  void _emitDeleteDirectory(RelativePath directory) {
+    logForTesting?.call('Watch,$watchedDirectory,deleteDirectory,$directory');
+    _directories.remove(directory)!.emitDeleteTree();
+  }
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/macos/event_tree.dart b/pkgs/watcher/lib/src/directory_watcher/macos/event_tree.dart
new file mode 100644
index 0000000..5203235
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/macos/event_tree.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import '../../unix_paths.dart';
+
+/// Tree of event paths relative to the watched path.
+///
+/// If [isSingleEvent] then there is an event at the current path. Because
+/// changed directories must be fully polled, events "under" the current path
+/// are not useful, and are discarded from the tree.
+class EventTree {
+  Map<PathSegment, EventTree>? _events;
+
+  EventTree() : _events = {};
+  EventTree.singleEvent() : _events = null;
+
+  /// Adds an event at [path].
+  ///
+  /// If there are already events under [path], that part of the tree has
+  /// [isSingleEvent] set and events under it are discarded.
+  ///
+  /// If there is already an event for a parent of [path], it is discarded
+  /// instead of added.
+  void add(RelativePath path) {
+    final segments = path.segments;
+    var current = this;
+
+    for (final segment in segments) {
+      final events = current._events;
+      if (events == null) {
+        // There is already an event for a parent of [path], discard the
+        // new event.
+        return;
+      }
+      // Add to the tree for [segment].
+      current = events.putIfAbsent(segment, EventTree.new);
+    }
+
+    // Mark [path] as a [singleEvent] and discard any events under it.
+    current._events = null;
+  }
+
+  /// Whether this event tree is actually a single event.
+  ///
+  /// There can be no events under it; [entries] will throw.
+  bool get isSingleEvent => _events == null;
+
+  /// Returns the event tree at [segment], or `null` if there is none.
+  EventTree? operator [](PathSegment segment) => _events?[segment];
+
+  /// Returns child event trees by path segment.
+  ///
+  /// Throws if [isSingleEvent].
+  Iterable<MapEntry<PathSegment, EventTree>> get entries => _events!.entries;
+
+  @override
+  String toString() => _events == null ? 'event' : '$_events';
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/macos/native_watch.dart b/pkgs/watcher/lib/src/directory_watcher/macos/native_watch.dart
new file mode 100644
index 0000000..106a45d
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/macos/native_watch.dart
@@ -0,0 +1,92 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import '../../event.dart';
+import '../../unix_paths.dart';
+import '../../utils.dart';
+import 'event_tree.dart';
+
+/// Watches a directory tree with the native MacOS watcher.
+///
+/// Handles incorrect closure of the watch due to a delete event from before
+/// the watch started, by re-opening the watch if the directory still exists.
+/// See https://github.com/dart-lang/sdk/issues/14373.
+class NativeWatch {
+  final AbsolutePath watchedDirectory;
+
+  /// Called when [watchedDirectory] is recreated.
+  final void Function() _watchedDirectoryWasRecreated;
+
+  /// Called when [watchedDirectory] is deleted.
+  final void Function() _watchedDirectoryWasDeleted;
+
+  /// Called with trees of events.
+  final void Function(EventTree events) _onEvents;
+
+  /// Called with native watch errors.
+  final void Function(Object, StackTrace) _onError;
+
+  StreamSubscription<List<Event>>? _subscription;
+
+  /// Watches [watchedDirectory].
+  ///
+  /// Pass [watchedDirectoryWasDeleted], [onEvents] and [onError] handlers.
+  NativeWatch({
+    required this.watchedDirectory,
+    required void Function() watchedDirectoryWasRecreated,
+    required void Function() watchedDirectoryWasDeleted,
+    required void Function(EventTree) onEvents,
+    required void Function(Object, StackTrace) onError,
+  })  : _onError = onError,
+        _onEvents = onEvents,
+        _watchedDirectoryWasRecreated = watchedDirectoryWasRecreated,
+        _watchedDirectoryWasDeleted = watchedDirectoryWasDeleted {
+    logForTesting?.call('NativeWatch(),$watchedDirectory');
+    _watch();
+  }
+
+  void _watch() {
+    _subscription?.cancel();
+    _subscription = watchedDirectory
+        .watch(recursive: true)
+        .batchAndConvertEvents()
+        .listen(_onData, onError: _onError, onDone: _onDone);
+  }
+
+  /// Closes the watch.
+  void close() {
+    logForTesting?.call('NativeWatch,$watchedDirectory,close');
+    _subscription?.cancel();
+    _subscription = null;
+  }
+
+  void _onData(List<Event> events) {
+    logForTesting?.call('NativeWatch,$watchedDirectory,onData,$events');
+    final eventTree = EventTree();
+    for (final event in events) {
+      // Delete of the watched directory is handled when the stream closes.
+      if (event.type == EventType.delete &&
+          event.absolutePath == watchedDirectory) {
+        continue;
+      }
+      eventTree.add(event.absolutePath.relativeTo(watchedDirectory));
+    }
+    _onEvents(eventTree);
+  }
+
+  void _onDone() {
+    logForTesting?.call('NativeWatch,$watchedDirectory,onDone');
+    // Check whether the directory exists and report if it was deleted or
+    // recreated. If it was recreated, restart the watch.
+    if (watchedDirectory.typeSync() == FileSystemEntityType.directory) {
+      _watchedDirectoryWasRecreated();
+      _watch();
+    } else {
+      _watchedDirectoryWasDeleted();
+    }
+  }
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/macos/watched_directory_tree.dart b/pkgs/watcher/lib/src/directory_watcher/macos/watched_directory_tree.dart
new file mode 100644
index 0000000..b69ea84
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/macos/watched_directory_tree.dart
@@ -0,0 +1,122 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import '../../unix_paths.dart';
+import '../../utils.dart';
+import '../../watch_event.dart';
+import 'directory_tree.dart';
+import 'event_tree.dart';
+import 'native_watch.dart';
+
+/// MacOS directory watcher using a [DirectoryTree].
+///
+/// MacOS events from a native watcher can arrive out of order, including in
+/// different batches. For example, a modification of `a/1` followed by a
+/// move of `a` can be reported as a delete of `a` then in a later batch of
+/// events a modification of `a/1`.
+///
+/// `WatchedDirectoryTree` reports correct events by polling based on event
+/// path to determine and report the actual current state. If a directory is
+/// mentioned then the whole directory is polled, if a file is mentioned then
+/// just the file is polled.
+class WatchedDirectoryTree {
+  final AbsolutePath watchedDirectory;
+  final StreamController<WatchEvent> _eventsController;
+  final Completer<void> _readyCompleter;
+
+  late final NativeWatch nativeWatch;
+  late final DirectoryTree directoryTree;
+
+  WatchedDirectoryTree(
+      {required String watchedDirectory,
+      required Completer<void> readyCompleter,
+      required StreamController<WatchEvent> eventsController})
+      : _readyCompleter = readyCompleter,
+        _eventsController = eventsController,
+        watchedDirectory = AbsolutePath(watchedDirectory) {
+    logForTesting?.call('WatchedDirectoryTree(),$watchedDirectory');
+    _watch();
+  }
+
+  void _watch() async {
+    nativeWatch = NativeWatch(
+      watchedDirectory: watchedDirectory,
+      watchedDirectoryWasRecreated: _watchedDirectoryWasRecreated,
+      watchedDirectoryWasDeleted: _watchedDirectoryWasDeleted,
+      onEvents: _onEvents,
+      onError: _emitError,
+    );
+    directoryTree =
+        DirectoryTree(watchedDirectory: watchedDirectory, emitEvent: _emit);
+
+    // The native watcher can emit events from before the watch started. Add
+    // a delay before marking "ready" to allow those events to arrive and be
+    // discarded.
+    //
+    // See https://github.com/dart-lang/sdk/issues/14373.
+    await Future<void>.delayed(const Duration(milliseconds: 200));
+    _ready();
+  }
+
+  /// Stops watching and closes the event stream.
+  void stopWatching() {
+    logForTesting?.call('WatchedDirectoryTree,$watchedDirectory,stopWatching');
+    _ready();
+    nativeWatch.close();
+    _eventsController.close();
+  }
+
+  /// Handler for when [watchedDirectory] is recreated.
+  void _watchedDirectoryWasRecreated() {
+    logForTesting?.call(
+        'WatchedDirectoryTree,$watchedDirectory,_watchedDirectoryWasRecreated');
+    // Poll the whole directory and emit events.
+    directoryTree.poll(EventTree.singleEvent());
+  }
+
+  /// Handler for when [watchedDirectory] is deleted.
+  void _watchedDirectoryWasDeleted() {
+    logForTesting?.call(
+        'WatchedDirectoryTree,$watchedDirectory,_watchedDirectoryWasDeleted');
+    _ready();
+    nativeWatch.close();
+    directoryTree.emitDeleteTree();
+    _eventsController.close();
+  }
+
+  /// Emits [event] on the event stream.
+  ///
+  /// If the watcher is not yet ready the event is discarded instead.
+  void _emit(WatchEvent event) {
+    logForTesting?.call('WatchedDirectoryTree,$watchedDirectory,_emit,$event');
+    if (_readyCompleter.isCompleted && !_eventsController.isClosed) {
+      _eventsController.add(event);
+    }
+  }
+
+  /// Emits [e] with stack trace [s] on the event stream.
+  void _emitError(Object e, StackTrace s) {
+    logForTesting?.call('WatchedDirectoryTree,$watchedDirectory,_emitError,$e');
+    _ready();
+    if (!_eventsController.isClosed) {
+      _eventsController.addError(e, s);
+      _eventsController.close();
+    }
+    nativeWatch.close();
+  }
+
+  /// Marks the watcher as ready, meaning it has done initial setup and is now
+  /// emitting events.
+  void _ready() {
+    if (!_readyCompleter.isCompleted) {
+      _readyCompleter.complete();
+    }
+  }
+
+  void _onEvents(EventTree events) {
+    directoryTree.poll(events);
+  }
+}
diff --git a/pkgs/watcher/lib/src/unix_paths.dart b/pkgs/watcher/lib/src/unix_paths.dart
new file mode 100644
index 0000000..45f3a85
--- /dev/null
+++ b/pkgs/watcher/lib/src/unix_paths.dart
@@ -0,0 +1,113 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import 'event.dart';
+import 'watch_event.dart';
+
+/// An absolute file path.
+extension type AbsolutePath(String _string) {
+  /// Whether this immediate parent directory of this path is [directory].
+  bool isIn(AbsolutePath directory) => p.dirname(_string) == directory._string;
+
+  /// This path relative to [root].
+  ///
+  /// Returns the empty string if this path is [root].
+  ///
+  /// Otherwise, throws if this path does not start with [root].
+  RelativePath relativeTo(AbsolutePath root) {
+    if (!_string.startsWith(root._string)) {
+      throw ArgumentError('$this relativeTo $root');
+    }
+    if (_string == root._string) return RelativePath('');
+    return RelativePath(_string.substring(root._string.length + 1));
+  }
+
+  /// This path relative to [root] as a single segment.
+  ///
+  /// Throws if this path is not a single segment under [root].
+  PathSegment segmentRelativeTo(AbsolutePath root) {
+    if (!_string.startsWith(root._string)) {
+      throw ArgumentError('$this segmentRelativeTo $root');
+    }
+    if (_string == root._string) throw ArgumentError(root);
+    final result = _string.substring(root._string.length + 1);
+    return PathSegment(result);
+  }
+
+  /// The last path segment of this path.
+  RelativePath get basename => RelativePath(p.basename(_string));
+
+  /// Lists the directory at this path, ignoring symlinks.
+  List<FileSystemEntity> listSync() =>
+      Directory(_string).listSync(followLinks: false);
+
+  /// Watches the directory at this path.
+  Stream<FileSystemEvent> watch({bool recursive = false}) =>
+      Directory(_string).watch(recursive: recursive);
+
+  /// Gets the [FileSystemEntityType] for this path.
+  FileSystemEntityType typeSync() =>
+      FileSystemEntity.typeSync(_string, followLinks: false);
+
+  /// Returns this path followed by [path].
+  AbsolutePath append(RelativePath path) =>
+      AbsolutePath('$_string/${path._string}');
+
+  /// Add event for this path.
+  WatchEvent get addEvent => WatchEvent(ChangeType.ADD, _string);
+
+  /// Modify event for this path.
+  WatchEvent get modifyEvent => WatchEvent(ChangeType.MODIFY, _string);
+
+  /// Remove event for this path.
+  WatchEvent get removeEvent => WatchEvent(ChangeType.REMOVE, _string);
+}
+
+extension FileSystemEntityExtensions on FileSystemEntity {
+  /// The event path relative to [root].
+  ///
+  /// Throws if not under [root].
+  RelativePath pathRelativeTo(AbsolutePath root) =>
+      AbsolutePath(path).relativeTo(root);
+
+  /// The path segment under [root].
+  ///
+  /// Throws if not a single path segment under [root].
+  PathSegment pathSegmentRelativeTo(AbsolutePath root) =>
+      AbsolutePath(path).segmentRelativeTo(root);
+}
+
+extension EventExtensions on Event {
+  /// The event [path] as an [AbsolutePath].
+  AbsolutePath get absolutePath => AbsolutePath(path);
+
+  /// The event [path] relative to [root].
+  RelativePath pathRelativeTo(AbsolutePath root) =>
+      AbsolutePath(path).relativeTo(root);
+
+  /// Whether the event path parent directory is exactly [directory].
+  bool isIn(AbsolutePath directory) => AbsolutePath(path).isIn(directory);
+}
+
+/// A relative file path.
+extension type RelativePath(String _string) {
+  List<PathSegment> get segments => _string.isEmpty
+      ? const <PathSegment>[]
+      : _string.split('/') as List<PathSegment>;
+}
+
+/// A path segment.
+extension type PathSegment._(String _string) implements RelativePath {
+  factory PathSegment(String segment) {
+    if (segment.isEmpty) throw ArgumentError('Segment cannot be empty.');
+    if (segment.contains('/')) {
+      throw ArgumentError('Segment cannot contain `/`.', segment);
+    }
+    return PathSegment._(segment);
+  }
+}
diff --git a/pkgs/watcher/test/custom_watcher_factory_test.dart b/pkgs/watcher/test/custom_watcher_factory_test.dart
index e9d65bb..6ef25ff 100644
--- a/pkgs/watcher/test/custom_watcher_factory_test.dart
+++ b/pkgs/watcher/test/custom_watcher_factory_test.dart
@@ -36,16 +36,16 @@
   });
 
   test('notifies for directories', () async {
-    var watcher = DirectoryWatcher('dir');
+    var watcher = DirectoryWatcher('/dir');
 
     var completer = Completer<WatchEvent>();
     watcher.events.listen((event) => completer.complete(event));
     await watcher.ready;
-    memFs.add('dir');
+    memFs.add('/dir');
     var event = await completer.future;
 
     expect(event.type, ChangeType.ADD);
-    expect(event.path, 'dir');
+    expect(event.path, '/dir');
   });
 
   test('registering twice throws', () async {
@@ -65,7 +65,7 @@
     registerCustomWatcher('Different id', watcherFactory.createDirectoryWatcher,
         watcherFactory.createFileWatcher);
     expect(() => FileWatcher('file.txt'), throwsA(isA<StateError>()));
-    expect(() => DirectoryWatcher('dir'), throwsA(isA<StateError>()));
+    expect(() => DirectoryWatcher('/dir'), throwsA(isA<StateError>()));
   });
 }
 
diff --git a/pkgs/watcher/test/directory_watcher/end_to_end_test_runner.dart b/pkgs/watcher/test/directory_watcher/end_to_end_test_runner.dart
index 85b009f..d18add0 100644
--- a/pkgs/watcher/test/directory_watcher/end_to_end_test_runner.dart
+++ b/pkgs/watcher/test/directory_watcher/end_to_end_test_runner.dart
@@ -45,8 +45,10 @@
 
   // Turn on logging of the watchers.
   final log = <LogEntry>[];
-  logForTesting = (message) =>
-      log.add(LogEntry('W ${message.replaceAll('${temp.path}/', '')}'));
+  logForTesting = (message) {
+    message = message.replaceAll('${temp.path}/', '').replaceAll(temp.path, '');
+    log.add(LogEntry('W $message'));
+  };
 
   // Create the watcher and [ClientSimulator].
   final watcher = createWatcher(path: temp.path);
@@ -72,6 +74,9 @@
       log.addAll(await changer.replayLog(replayLog));
     }
 
+    // Short fixed delay so tester file reads don't race with writes.
+    await Future<void>.delayed(const Duration(milliseconds: 50));
+
     // Give time for events to arrive. To allow tests to run quickly when the
     // events are handled quickly, poll and continue if verification passes.
     var succeeded = false;
@@ -138,12 +143,17 @@
   final teardowns = <void Function()>[];
   try {
     if (replay) {
+      final filteredTestCases = testCases;
+      if (specifiedName != null) {
+        filteredTestCases
+            .retainWhere((testCase) => testCase.name == specifiedName);
+      }
+      if (filteredTestCases.isEmpty) {
+        throw ArgumentError('No test case matching `$specifiedName`.');
+      }
       while (true) {
         stdout.write('.');
-        for (final testCase in testCases) {
-          if (specifiedName != null && specifiedName != testCase.name) {
-            continue;
-          }
+        for (final testCase in filteredTestCases) {
           await runTest(
             name: testCase.name,
             addTearDown: teardowns.add,
diff --git a/pkgs/watcher/test/directory_watcher/end_to_end_tests.dart b/pkgs/watcher/test/directory_watcher/end_to_end_tests.dart
index e56cbc9..5e0b212 100644
--- a/pkgs/watcher/test/directory_watcher/end_to_end_tests.dart
+++ b/pkgs/watcher/test/directory_watcher/end_to_end_tests.dart
@@ -36,6 +36,60 @@
 }
 
 final testCases = [
+  TestCase('many directories, move directory to recently moved directory', '''
+F create directory,f/a
+F create,f/a/78670,387
+F create directory,g/f
+F create directory,b/c
+F create directory,g/j
+F create directory,f/d
+F create directory,i
+F create directory,h/i
+F create directory,c/h
+F create directory,g/c
+F create directory,g/a
+F create directory,e/c
+F move directory to new,f/d,b/h
+F create directory,j/c
+F move directory to new,b/h,d/h
+F create directory,d/i
+F move directory to new,b/g,g/h
+F create,j/c/73241,207
+F move file to new,42776,61386
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F wait
+F create directory,d/b
+F create,d/b/56283,667
+F create directory,g
+F move directory to new,g,d/a
+F create directory,j/j
+F move file to new,i/63276,j/j/66963
+F modify,f/8110,250
+F create,52267,858
+F move directory to new,f,g
+'''),
+  TestCase(
+    'moves and modifies',
+    '''
+F create directory,1
+F create,1/1,1
+${_movesAndModifies()}
+''',
+  ),
   TestCase(
     'move directory in, move file over',
     '''
@@ -79,3 +133,12 @@
 ''',
   ),
 ];
+
+String _movesAndModifies() {
+  final result = StringBuffer();
+  for (var i = 1; i != 50; ++i) {
+    result.writeln('F move directory to new,$i,${i + 1}');
+    result.writeln('F modify,${i + 1}/1,$i');
+  }
+  return result.toString();
+}
diff --git a/pkgs/watcher/test/directory_watcher/file_tests.dart b/pkgs/watcher/test/directory_watcher/file_tests.dart
index 827ac14..89f6967 100644
--- a/pkgs/watcher/test/directory_watcher/file_tests.dart
+++ b/pkgs/watcher/test/directory_watcher/file_tests.dart
@@ -429,10 +429,16 @@
       renameDir('sub', 'dir/sub');
 
       if (isNative) {
-        await inAnyOrder(withPermutations(
-            (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
-        await inAnyOrder(withPermutations(
-            (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+        if (Platform.isMacOS) {
+          // MacOS watcher reports as "modify" instead of remove then add.
+          await inAnyOrder(withPermutations(
+              (i, j, k) => isModifyEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+        } else {
+          await inAnyOrder(withPermutations(
+              (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+          await inAnyOrder(withPermutations(
+              (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+        }
       } else {
         // Polling watchers can't detect this as directory contents mtimes
         // aren't updated when the directory is moved.
@@ -471,6 +477,25 @@
       await expectNoEvents();
     });
 
+    test('multiple deletes order is respected', () async {
+      createDir('watched');
+      writeFile('a/1');
+      writeFile('b/1');
+
+      await startWatcher(path: 'watched');
+
+      renameDir('a', 'watched/x');
+      renameDir('watched/x', 'a');
+      renameDir('b', 'watched/x');
+      writeFile('watched/x/1', contents: 'updated');
+      // This is a "duplicate" delete of x, but it's not the same delete and the
+      // watcher needs to notice that it happens after the update to x/1 so
+      // there is no file left behind.
+      renameDir('watched/x', 'b');
+
+      await expectNoEvents();
+    });
+
     test('subdirectory watching is robust against races', () async {
       // Make sandboxPath accessible to child isolates created by Isolate.run.
       final sandboxPath = d.sandbox;
diff --git a/pkgs/watcher/test/directory_watcher/macos/event_tree_test.dart b/pkgs/watcher/test/directory_watcher/macos/event_tree_test.dart
new file mode 100644
index 0000000..149c28a
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/macos/event_tree_test.dart
@@ -0,0 +1,55 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/macos/event_tree.dart';
+import 'package:watcher/src/unix_paths.dart';
+
+void main() {
+  group('EventTree', () {
+    test('empty event tree is not an event', () {
+      expect(EventTree().isSingleEvent, false);
+    });
+
+    test('event tree with event at root is an event', () {
+      final eventTree = EventTree();
+      eventTree.add(RelativePath(''));
+      expect(eventTree.isSingleEvent, true);
+    });
+
+    test('event tree with event under root has expected single event', () {
+      final eventTree = EventTree();
+      eventTree.add(RelativePath('a'));
+      expect(eventTree.isSingleEvent, false);
+
+      expect(eventTree[PathSegment('a')]!.isSingleEvent, true);
+    });
+
+    test('event tree with event deep under root has expected single event', () {
+      final eventTree = EventTree();
+      eventTree.add(RelativePath('a/b'));
+      expect(eventTree.isSingleEvent, false);
+
+      expect(eventTree[PathSegment('a')]!.isSingleEvent, false);
+      expect(
+          eventTree[PathSegment('a')]![PathSegment('b')]!.isSingleEvent, true);
+    });
+
+    test('adding event removes tree under it', () {
+      final eventTree = EventTree();
+      eventTree.add(RelativePath('a/b'));
+      eventTree.add(RelativePath('a'));
+
+      expect(eventTree[PathSegment('a')]![PathSegment('b')], null);
+    });
+
+    test("events can't be added under an event", () {
+      final eventTree = EventTree();
+      eventTree.add(RelativePath('a'));
+      eventTree.add(RelativePath('a/b'));
+
+      expect(eventTree[PathSegment('a')]![PathSegment('b')], null);
+    });
+  });
+}
diff --git a/pkgs/watcher/test/directory_watcher/mac_os_test.dart b/pkgs/watcher/test/directory_watcher/macos_test.dart
similarity index 78%
rename from pkgs/watcher/test/directory_watcher/mac_os_test.dart
rename to pkgs/watcher/test/directory_watcher/macos_test.dart
index 512fc86..c55d880 100644
--- a/pkgs/watcher/test/directory_watcher/mac_os_test.dart
+++ b/pkgs/watcher/test/directory_watcher/macos_test.dart
@@ -6,7 +6,7 @@
 library;
 
 import 'package:test/test.dart';
-import 'package:watcher/src/directory_watcher/mac_os.dart';
+import 'package:watcher/src/directory_watcher/macos.dart';
 import 'package:watcher/watcher.dart';
 
 import '../utils.dart';
@@ -15,13 +15,13 @@
 import 'link_tests.dart';
 
 void main() {
-  watcherFactory = MacOSDirectoryWatcher.new;
+  watcherFactory = MacosDirectoryWatcher.new;
 
   fileTests(isNative: true);
   linkTests(isNative: true);
   endToEndTests();
 
   test('DirectoryWatcher creates a MacOSDirectoryWatcher on Mac OS', () {
-    expect(DirectoryWatcher('.'), const TypeMatcher<MacOSDirectoryWatcher>());
+    expect(DirectoryWatcher('.'), const TypeMatcher<MacosDirectoryWatcher>());
   });
 }
diff --git a/pkgs/watcher/test/no_subscription/mac_os_test.dart b/pkgs/watcher/test/no_subscription/macos_test.dart
similarity index 77%
rename from pkgs/watcher/test/no_subscription/mac_os_test.dart
rename to pkgs/watcher/test/no_subscription/macos_test.dart
index 55a8308..03f729e 100644
--- a/pkgs/watcher/test/no_subscription/mac_os_test.dart
+++ b/pkgs/watcher/test/no_subscription/macos_test.dart
@@ -6,13 +6,13 @@
 library;
 
 import 'package:test/test.dart';
-import 'package:watcher/src/directory_watcher/mac_os.dart';
+import 'package:watcher/src/directory_watcher/macos.dart';
 
 import '../utils.dart';
 import 'shared.dart';
 
 void main() {
-  watcherFactory = MacOSDirectoryWatcher.new;
+  watcherFactory = MacosDirectoryWatcher.new;
 
   sharedTests();
 }
diff --git a/pkgs/watcher/test/ready/mac_os_test.dart b/pkgs/watcher/test/ready/macos_test.dart
similarity index 77%
rename from pkgs/watcher/test/ready/mac_os_test.dart
rename to pkgs/watcher/test/ready/macos_test.dart
index 55a8308..03f729e 100644
--- a/pkgs/watcher/test/ready/mac_os_test.dart
+++ b/pkgs/watcher/test/ready/macos_test.dart
@@ -6,13 +6,13 @@
 library;
 
 import 'package:test/test.dart';
-import 'package:watcher/src/directory_watcher/mac_os.dart';
+import 'package:watcher/src/directory_watcher/macos.dart';
 
 import '../utils.dart';
 import 'shared.dart';
 
 void main() {
-  watcherFactory = MacOSDirectoryWatcher.new;
+  watcherFactory = MacosDirectoryWatcher.new;
 
   sharedTests();
 }