Implement the ability to register custom watcher implementations

This allows registering a special-purpose factory that returns its own
`Watcher` implementation that will take precedence over the default
ones. The main motivation for this is handling of file systems that need
custom code to watch for changes.
diff --git a/pkgs/watcher/lib/src/custom_watcher_factory.dart b/pkgs/watcher/lib/src/custom_watcher_factory.dart
new file mode 100644
index 0000000..ffac06b
--- /dev/null
+++ b/pkgs/watcher/lib/src/custom_watcher_factory.dart
@@ -0,0 +1,46 @@
+import '../watcher.dart';
+
+/// Defines a way to create a custom watcher instead of the default ones.
+///
+/// This will be used when a [DirectoryWatcher] or [FileWatcher] would be
+/// created and will take precedence over the default ones.
+abstract class CustomWatcherFactory {
+  /// Uniquely identify this watcher.
+  String get id;
+
+  /// Tries to create a [DirectoryWatcher] for the provided path.
+  ///
+  /// Should return `null` if the path is not supported by this factory.
+  DirectoryWatcher createDirectoryWatcher(String path, {Duration pollingDelay});
+
+  /// Tries to create a [FileWatcher] for the provided path.
+  ///
+  /// Should return `null` if the path is not supported by this factory.
+  FileWatcher createFileWatcher(String path, {Duration pollingDelay});
+}
+
+/// Registers a custom watcher.
+///
+/// It's only allowed to register a watcher once per [id]. The [supportsPath]
+/// will be called to determine if the [createWatcher] should be used instead of
+/// the built-in watchers.
+///
+/// Note that we will try [CustomWatcherFactory] one by one in the order they
+/// were registered.
+void registerCustomWatcherFactory(CustomWatcherFactory customFactory) {
+  if (_customWatcherFactories.containsKey(customFactory.id)) {
+    throw ArgumentError('A custom watcher with id `${customFactory.id}` '
+        'has already been registered');
+  }
+  _customWatcherFactories[customFactory.id] = customFactory;
+}
+
+/// Unregisters a custom watcher and returns it (returns `null` if it was never
+/// registered).
+CustomWatcherFactory unregisterCustomWatcherFactory(String id) =>
+    _customWatcherFactories.remove(id);
+
+Iterable<CustomWatcherFactory> get customWatcherFactories =>
+    _customWatcherFactories.values;
+
+final _customWatcherFactories = <String, CustomWatcherFactory>{};
diff --git a/pkgs/watcher/lib/src/directory_watcher.dart b/pkgs/watcher/lib/src/directory_watcher.dart
index e0ef3fc..858d020 100644
--- a/pkgs/watcher/lib/src/directory_watcher.dart
+++ b/pkgs/watcher/lib/src/directory_watcher.dart
@@ -5,10 +5,11 @@
 import 'dart:io';
 
 import '../watcher.dart';
+import 'custom_watcher_factory.dart';
 import 'directory_watcher/linux.dart';
 import 'directory_watcher/mac_os.dart';
-import 'directory_watcher/windows.dart';
 import 'directory_watcher/polling.dart';
+import 'directory_watcher/windows.dart';
 
 /// Watches the contents of a directory and emits [WatchEvent]s when something
 /// in the directory has changed.
@@ -29,6 +30,14 @@
   /// watchers.
   factory DirectoryWatcher(String directory, {Duration pollingDelay}) {
     if (FileSystemEntity.isWatchSupported) {
+      for (var custom in customWatcherFactories) {
+        var watcher = custom.createDirectoryWatcher(directory,
+            pollingDelay: pollingDelay);
+        if (watcher != null) {
+          return watcher;
+        }
+      }
+
       if (Platform.isLinux) return LinuxDirectoryWatcher(directory);
       if (Platform.isMacOS) return MacOSDirectoryWatcher(directory);
       if (Platform.isWindows) return WindowsDirectoryWatcher(directory);
diff --git a/pkgs/watcher/lib/src/file_watcher.dart b/pkgs/watcher/lib/src/file_watcher.dart
index c4abddd..0b7afc7 100644
--- a/pkgs/watcher/lib/src/file_watcher.dart
+++ b/pkgs/watcher/lib/src/file_watcher.dart
@@ -5,6 +5,7 @@
 import 'dart:io';
 
 import '../watcher.dart';
+import 'custom_watcher_factory.dart';
 import 'file_watcher/native.dart';
 import 'file_watcher/polling.dart';
 
@@ -29,6 +30,13 @@
   /// and higher CPU usage. Defaults to one second. Ignored for non-polling
   /// watchers.
   factory FileWatcher(String file, {Duration pollingDelay}) {
+    for (var custom in customWatcherFactories) {
+      var watcher = custom.createFileWatcher(file, pollingDelay: pollingDelay);
+      if (watcher != null) {
+        return watcher;
+      }
+    }
+
     // [File.watch] doesn't work on Windows, but
     // [FileSystemEntity.isWatchSupported] is still true because directory
     // watching does work.
diff --git a/pkgs/watcher/lib/watcher.dart b/pkgs/watcher/lib/watcher.dart
index 107ac8f..f5c7d3e 100644
--- a/pkgs/watcher/lib/watcher.dart
+++ b/pkgs/watcher/lib/watcher.dart
@@ -5,15 +5,20 @@
 import 'dart:async';
 import 'dart:io';
 
-import 'src/watch_event.dart';
 import 'src/directory_watcher.dart';
 import 'src/file_watcher.dart';
+import 'src/watch_event.dart';
 
-export 'src/watch_event.dart';
+export 'src/custom_watcher_factory.dart'
+    show
+        CustomWatcherFactory,
+        registerCustomWatcherFactory,
+        unregisterCustomWatcherFactory;
 export 'src/directory_watcher.dart';
 export 'src/directory_watcher/polling.dart';
 export 'src/file_watcher.dart';
 export 'src/file_watcher/polling.dart';
+export 'src/watch_event.dart';
 
 abstract class Watcher {
   /// The path to the file or directory whose contents are being monitored.
diff --git a/pkgs/watcher/test/custom_watcher_factory_test.dart b/pkgs/watcher/test/custom_watcher_factory_test.dart
new file mode 100644
index 0000000..488b607
--- /dev/null
+++ b/pkgs/watcher/test/custom_watcher_factory_test.dart
@@ -0,0 +1,130 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:watcher/watcher.dart';
+
+void main() {
+  _MemFs memFs;
+
+  setUp(() {
+    memFs = _MemFs();
+    registerCustomWatcherFactory(_MemFsWatcherFactory(memFs));
+  });
+
+  tearDown(() async {
+    unregisterCustomWatcherFactory('MemFs');
+  });
+
+  test('notifes for files', () async {
+    var watcher = FileWatcher('file.txt');
+
+    var completer = Completer<WatchEvent>();
+    watcher.events.listen((event) => completer.complete(event));
+    await watcher.ready;
+    memFs.add('file.txt');
+    var event = await completer.future;
+
+    expect(event.type, ChangeType.ADD);
+    expect(event.path, 'file.txt');
+  });
+
+  test('notifes for directories', () async {
+    var watcher = DirectoryWatcher('dir');
+
+    var completer = Completer<WatchEvent>();
+    watcher.events.listen((event) => completer.complete(event));
+    await watcher.ready;
+    memFs.add('dir');
+    var event = await completer.future;
+
+    expect(event.type, ChangeType.ADD);
+    expect(event.path, 'dir');
+  });
+
+  test('unregister works', () async {
+    var memFactory = _MemFsWatcherFactory(memFs);
+    unregisterCustomWatcherFactory(memFactory.id);
+
+    var completer = Completer<dynamic>();
+    var watcher = FileWatcher('file.txt');
+    watcher.events.listen((e) {}, onError: (e) => completer.complete(e));
+    await watcher.ready;
+    memFs.add('file.txt');
+    var result = await completer.future;
+
+    expect(result, isA<FileSystemException>());
+  });
+
+  test('registering twice throws', () async {
+    expect(() => registerCustomWatcherFactory(_MemFsWatcherFactory(memFs)),
+        throwsA(isA<ArgumentError>()));
+  });
+}
+
+class _MemFs {
+  final _streams = <String, Set<StreamController<WatchEvent>>>{};
+
+  StreamController<WatchEvent> watchStream(String path) {
+    var controller = StreamController<WatchEvent>();
+    _streams.putIfAbsent(path, () => {}).add(controller);
+    return controller;
+  }
+
+  void add(String path) {
+    var controllers = _streams[path];
+    if (controllers != null) {
+      for (var controller in controllers) {
+        controller.add(WatchEvent(ChangeType.ADD, path));
+      }
+    }
+  }
+
+  void remove(String path) {
+    var controllers = _streams[path];
+    if (controllers != null) {
+      for (var controller in controllers) {
+        controller.add(WatchEvent(ChangeType.REMOVE, path));
+      }
+    }
+  }
+}
+
+class _MemFsWatcher implements FileWatcher, DirectoryWatcher, Watcher {
+  final String _path;
+  final StreamController<WatchEvent> _controller;
+
+  _MemFsWatcher(this._path, this._controller);
+
+  @override
+  String get path => _path;
+
+  @override
+  String get directory => throw UnsupportedError('directory is not supported');
+
+  @override
+  Stream<WatchEvent> get events => _controller.stream;
+
+  @override
+  bool get isReady => true;
+
+  @override
+  Future<void> get ready async {}
+}
+
+class _MemFsWatcherFactory implements CustomWatcherFactory {
+  final _MemFs _memFs;
+  _MemFsWatcherFactory(this._memFs);
+
+  @override
+  String get id => 'MemFs';
+
+  @override
+  DirectoryWatcher createDirectoryWatcher(String path,
+          {Duration pollingDelay}) =>
+      _MemFsWatcher(path, _memFs.watchStream(path));
+
+  @override
+  FileWatcher createFileWatcher(String path, {Duration pollingDelay}) =>
+      _MemFsWatcher(path, _memFs.watchStream(path));
+}