// 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; AbsolutePath get parent => AbsolutePath(p.dirname(_string)); /// This path relative to [root]. /// /// Returns the empty string if this path is [root]. /// /// Otherwise, return null if this path does not start with [root]. RelativePath? tryRelativeTo(AbsolutePath root) { if (!_string.startsWith(root._string)) return null; if (_string == root._string) return RelativePath(''); if (_string.substring(root._string.length, root._string.length + 1) != Platform.pathSeparator) { return null; } return RelativePath(_string.substring(root._string.length + 1)); } /// 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) { return tryRelativeTo(root) ?? (throw ArgumentError('$this relativeTo $root')); } /// 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 listSync() => Directory(_string).listSync(followLinks: false); /// Watches the directory at this path. Stream 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${Platform.pathSeparator}${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 get segments => _string.isEmpty ? const [] : _string.split(Platform.pathSeparator) as List; } /// 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(Platform.pathSeparator)) { throw ArgumentError( 'Segment cannot contain `${Platform.pathSeparator}`.', segment); } return PathSegment._(segment); } }