diff --git a/.github/workflows/watcher.yaml b/.github/workflows/watcher.yaml index 6a3b9ba70..0afb7079a 100644 --- a/.github/workflows/watcher.yaml +++ b/.github/workflows/watcher.yaml @@ -54,7 +54,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - sdk: [3.1, dev] + sdk: [3.3, dev] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c diff --git a/pkgs/watcher/lib/src/directory_watcher/linux.dart b/pkgs/watcher/lib/src/directory_watcher/linux.dart index 99e2cf50e..0f587cec3 100644 --- a/pkgs/watcher/lib/src/directory_watcher/linux.dart +++ b/pkgs/watcher/lib/src/directory_watcher/linux.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:async/async.dart'; import '../directory_watcher.dart'; +import '../event.dart'; import '../path_set.dart'; import '../resubscribable.dart'; import '../utils.dart'; @@ -81,7 +82,7 @@ class _LinuxDirectoryWatcher }))); // Batch the inotify changes together so that we can dedup events. - var innerStream = _nativeEvents.stream.batchEvents(); + var innerStream = _nativeEvents.stream.map(Event.new).batchEvents(); _listen(innerStream, _onBatch, onError: (Object error, StackTrace stackTrace) { // Guarantee that ready always completes. @@ -145,7 +146,7 @@ class _LinuxDirectoryWatcher } /// The callback that's run when a batch of changes comes in. - void _onBatch(List batch) { + void _onBatch(List batch) { var files = {}; var dirs = {}; var changed = {}; @@ -162,30 +163,40 @@ class _LinuxDirectoryWatcher changed.add(event.path); - if (event is FileSystemMoveEvent) { - files.remove(event.path); - dirs.remove(event.path); - - var destination = event.destination; - if (destination == null) continue; - - changed.add(destination); - if (event.isDirectory) { - files.remove(destination); - dirs.add(destination); - } else { - files.add(destination); - dirs.remove(destination); - } - } else if (event is FileSystemDeleteEvent) { - files.remove(event.path); - dirs.remove(event.path); - } else if (event.isDirectory) { - files.remove(event.path); - dirs.add(event.path); - } else { - files.add(event.path); - dirs.remove(event.path); + switch (event.type) { + case EventType.moveFile: + files.remove(event.path); + dirs.remove(event.path); + var destination = event.destination; + if (destination != null) { + changed.add(destination); + files.add(destination); + dirs.remove(destination); + } + + case EventType.moveDirectory: + files.remove(event.path); + dirs.remove(event.path); + var destination = event.destination; + if (destination != null) { + changed.add(destination); + files.remove(destination); + dirs.add(destination); + } + + case EventType.delete: + files.remove(event.path); + dirs.remove(event.path); + + case EventType.createDirectory: + case EventType.modifyDirectory: + files.remove(event.path); + dirs.add(event.path); + + case EventType.createFile: + case EventType.modifyFile: + files.add(event.path); + dirs.remove(event.path); } } diff --git a/pkgs/watcher/lib/src/event.dart b/pkgs/watcher/lib/src/event.dart new file mode 100644 index 000000000..159106d64 --- /dev/null +++ b/pkgs/watcher/lib/src/event.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 'dart:io'; + +/// Extension type replacing [FileSystemEvent] for `package:watcher` internal +/// use. +/// +/// The [FileSystemDeleteEvent] subclass of [FileSystemEvent] does something +/// surprising for `isDirectory`: it always returns `false`. The constructor +/// accepts a boolean called `isDirectory` but discards it. +/// +/// So, this extension type hides `isDirectory` and instead provides an +/// [EventType] enum with the seven types of event actually used. +extension type Event(FileSystemEvent event) { + /// See [FileSystemEvent.path]. + String get path => event.path; + + EventType get type { + switch (event.type) { + case FileSystemEvent.create: + return event.isDirectory + ? EventType.createDirectory + : EventType.createFile; + case FileSystemEvent.delete: + return EventType.delete; + case FileSystemEvent.modify: + return event.isDirectory + ? EventType.modifyDirectory + : EventType.modifyFile; + case FileSystemEvent.move: + return event.isDirectory ? EventType.moveDirectory : EventType.moveFile; + default: + throw StateError('Invalid event type ${event.type}.'); + } + } + + /// See [FileSystemMoveEvent.destination]. + /// + /// For other types of event, always `null`. + String? get destination => event.type == FileSystemEvent.move + ? (event as FileSystemMoveEvent).destination + : null; +} + +/// See [FileSystemEvent.type]. +/// +/// This additionally encodes [FileSystemEvent.isDirectory], which is specified +/// for all event types except deletes. +enum EventType { + delete, + createFile, + createDirectory, + modifyFile, + modifyDirectory, + moveFile, + moveDirectory; +} diff --git a/pkgs/watcher/lib/src/file_watcher/native.dart b/pkgs/watcher/lib/src/file_watcher/native.dart index 28cf8a180..2fcf45b34 100644 --- a/pkgs/watcher/lib/src/file_watcher/native.dart +++ b/pkgs/watcher/lib/src/file_watcher/native.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:io'; +import '../event.dart'; import '../file_watcher.dart'; import '../resubscribable.dart'; import '../utils.dart'; @@ -33,7 +34,7 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher { Future get ready => _readyCompleter.future; final _readyCompleter = Completer(); - StreamSubscription>? _subscription; + StreamSubscription>? _subscription; /// On MacOS only, whether the file existed on startup. bool? _existedAtStartup; @@ -50,7 +51,7 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher { var file = File(path); // Batch the events together so that we can dedupe them. - var stream = file.watch().batchEvents(); + var stream = file.watch().map(Event.new).batchEvents(); if (Platform.isMacOS) { var existedAtStartupFuture = file.exists(); @@ -65,8 +66,8 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher { onError: _eventsController.addError, onDone: _onDone); } - void _onBatch(List batch) { - if (batch.any((event) => event.type == FileSystemEvent.delete)) { + void _onBatch(List batch) { + if (batch.any((event) => event.type == EventType.delete)) { // If the file is deleted, the underlying stream will close. We handle // emitting our own REMOVE event in [_onDone]. return; @@ -77,7 +78,7 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher { // created just before the `watch`. If the file existed at startup then it // should be ignored. if (_existedAtStartup! && - batch.every((event) => event.type == FileSystemEvent.create)) { + batch.every((event) => event.type == EventType.createFile)) { return; } } diff --git a/pkgs/watcher/pubspec.yaml b/pkgs/watcher/pubspec.yaml index c7119052d..fde8fe3dd 100644 --- a/pkgs/watcher/pubspec.yaml +++ b/pkgs/watcher/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/dart-lang/tools/tree/main/pkgs/watcher issue_tracker: https://github.com/dart-lang/tools/labels/package%3Awatcher environment: - sdk: ^3.1.0 + sdk: ^3.3.0 dependencies: async: ^2.5.0