| // Copyright (c) 2015, 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:collection'; |
| import 'dart:core'; |
| |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:watcher/watcher.dart'; |
| |
| /** |
| * A function called when a watch [event] associated with a watched resource is |
| * received. The list of [tokens] will contain all of the tokens associated with |
| * folders containing (or the same as) the watched resource. |
| */ |
| typedef void HandleWatchEvent<T>(WatchEvent event, List<T> tokens); |
| |
| /** |
| * An object that manages a collections of folders that need to be watched in |
| * order to ensure that we are watching the minimum number of folders. |
| * |
| * Each folder can be watched multiple times. In order to differentiate between |
| * the watch requests, each watch request has a *token* associated with it. The |
| * tokens that are used must correctly implement both [==] and [hashCode]. |
| */ |
| class WatchManager<T> { |
| /** |
| * The resource provider used to convert paths to resources. |
| */ |
| final ResourceProvider provider; |
| |
| /** |
| * The function that is invoked when a watch event is received. |
| */ |
| final HandleWatchEvent<T> handleWatchEvent; |
| |
| /** |
| * A node representing the (conceptual) root of all other folders. |
| */ |
| final WatchNode<T> rootNode = new WatchNode<T>(null); |
| |
| /** |
| * A table mapping the folders that are being watched to the nodes |
| * representing those folders. |
| */ |
| final Map<Folder, WatchNode<T>> _watchedFolders = |
| new HashMap<Folder, WatchNode<T>>(); |
| |
| /** |
| * Initialize a newly created watch manager to use the resource [provider] to |
| * convert file paths to resources and to call the [handleWatchEvent] function |
| * to notify the owner of the manager when resources have been changed. |
| */ |
| WatchManager(this.provider, this.handleWatchEvent); |
| |
| /** |
| * Record the fact that we are now watching the given [folder], and associate |
| * that folder with the given [token]. If the folder is already being watched |
| * and is already associated with the token, then this request is effectively |
| * ignored. |
| */ |
| void addFolder(Folder folder, T token) { |
| WatchNode<T> folderNode = _watchedFolders[folder]; |
| // |
| // If the folder was already being watched, just record the new token. |
| // |
| if (folderNode != null) { |
| folderNode.tokens.add(token); |
| return; |
| } |
| // |
| // Otherwise, add the folder to the tree. |
| // |
| folderNode = new WatchNode<T>(folder); |
| _watchedFolders[folder] = folderNode; |
| folderNode.tokens.add(token); |
| WatchNode<T> parentNode = rootNode.insert(folderNode); |
| // |
| // If we are not watching a folder that contains the folder, then create a |
| // subscription for it. |
| // |
| if (parentNode == rootNode) { |
| folderNode.subscription = folder.changes.listen(_handleWatchEvent); |
| // |
| // Any nodes that became children of the newly added folder would have |
| // been top-level folders and would have been watched. We need to cancel |
| // their subscriptions. |
| // |
| for (WatchNode<T> childNode in folderNode.children) { |
| assert(childNode.subscription != null); |
| if (childNode.subscription != null) { |
| childNode.subscription.cancel(); |
| childNode.subscription = null; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Record that we are no longer watching the given [folder] with the given |
| * [token]. |
| * |
| * Throws a [StateError] if the folder is not be watched or is not associated |
| * with the given token. |
| */ |
| void removeFolder(Folder folder, T token) { |
| WatchNode<T> folderNode = _watchedFolders[folder]; |
| if (folderNode == null) { |
| assert(false); |
| return; |
| } |
| Set<T> tokens = folderNode.tokens; |
| if (!tokens.remove(token)) { |
| assert(false); |
| } |
| // |
| // If this was the last token associated with this folder, then remove the |
| // folder from the tree. |
| // |
| if (tokens.isEmpty) { |
| // |
| // If the folder was a top-level folder, then we need to create |
| // subscriptions for all of its children and cancel its subscription. |
| // |
| if (folderNode.subscription != null) { |
| for (WatchNode<T> childNode in folderNode.children) { |
| assert(childNode.subscription == null); |
| childNode.subscription = |
| childNode.folder.changes.listen(_handleWatchEvent); |
| } |
| folderNode.subscription.cancel(); |
| folderNode.subscription = null; |
| } |
| folderNode.delete(); |
| _watchedFolders.remove(folder); |
| } |
| } |
| |
| /** |
| * Dispatch the given event by finding all of the tokens that contain the |
| * resource and invoke the [handleWatchEvent] function. |
| */ |
| void _handleWatchEvent(WatchEvent event) { |
| String path = event.path; |
| List<T> tokens = <T>[]; |
| WatchNode<T> parent = rootNode.findParent(path); |
| while (parent != rootNode) { |
| tokens.addAll(parent.tokens); |
| parent = parent.parent; |
| } |
| if (tokens.isNotEmpty) { |
| handleWatchEvent(event, tokens); |
| } |
| } |
| } |
| |
| /** |
| * The information kept by a [WatchManager] about a single folder that is being |
| * watched. |
| * |
| * Watch nodes form a tree in which one node is a child of another node if the |
| * child's folder is contained in the parent's folder and none of the folders |
| * between the parent's folder and the child's folder are being watched. |
| */ |
| class WatchNode<T> { |
| /** |
| * The folder for which information is being maintained. This is `null` for |
| * the unique "root" node that maintains references to all of the top-level |
| * folders being watched. |
| */ |
| final Folder folder; |
| |
| /** |
| * The parent of this node. |
| */ |
| WatchNode<T> parent; |
| |
| /** |
| * The information for the children of this node. |
| */ |
| final List<WatchNode<T>> _children = <WatchNode<T>>[]; |
| |
| /** |
| * The tokens that were used to register interest in watching this folder. |
| */ |
| final Set<T> tokens = new HashSet<T>(); |
| |
| /** |
| * The subscription being used to watch the folder, or `null` if the folder |
| * is being watched as part of a containing folder (in other words, if the |
| * parent is not the special "root"). |
| */ |
| StreamSubscription<WatchEvent> subscription; |
| |
| /** |
| * Initialize a newly created node to represent the given [folder]. |
| */ |
| WatchNode(this.folder); |
| |
| /** |
| * Return a list containing the children of this node. |
| */ |
| Iterable<WatchNode<T>> get children => _children; |
| |
| /** |
| * Remove this node from the tree of watched folders. |
| */ |
| void delete() { |
| if (parent != null) { |
| parent._removeChild(this); |
| parent = null; |
| } |
| } |
| |
| /** |
| * Return the highest node reachable from this node that contains the given |
| * [filePath]. If no other node is found, return this node, even if this node |
| * does not contain the path. |
| */ |
| WatchNode<T> findParent(String filePath) { |
| if (_children == null) { |
| return this; |
| } |
| for (WatchNode<T> childNode in _children) { |
| if (childNode.folder.isOrContains(filePath)) { |
| return childNode.findParent(filePath); |
| } |
| } |
| return this; |
| } |
| |
| /** |
| * Insert the given [node] into the tree of watched folders, either as a child |
| * of this node or as a descendent of one of this node's children. Return the |
| * immediate parent of the newly added node. |
| */ |
| WatchNode<T> insert(WatchNode<T> node) { |
| WatchNode<T> parentNode = findParent(node.folder.path); |
| parentNode._addChild(node, true); |
| return parentNode; |
| } |
| |
| @override |
| String toString() => 'WatchNode (' |
| 'folder = ${folder == null ? '<root>' : folder.path}, ' |
| 'tokens = $tokens, ' |
| 'subscription = ${subscription == null ? 'null' : 'non-null'})'; |
| |
| /** |
| * Add the given [newChild] as an immediate child of this node. |
| * |
| * If [checkChildren] is `true`, check to see whether any of the previously |
| * existing children of this node should now be children of the new child, and |
| * if so, move them. |
| */ |
| void _addChild(WatchNode<T> newChild, bool checkChildren) { |
| if (checkChildren) { |
| Folder folder = newChild.folder; |
| for (int i = _children.length - 1; i >= 0; i--) { |
| WatchNode<T> existingChild = _children[i]; |
| if (folder.contains(existingChild.folder.path)) { |
| newChild._addChild(existingChild, false); |
| _children.removeAt(i); |
| } |
| } |
| } |
| newChild.parent = this; |
| _children.add(newChild); |
| } |
| |
| /** |
| * Remove the given [node] from the list of children of this node. Any |
| * children of the [node] will become children of this node. |
| */ |
| void _removeChild(WatchNode<T> child) { |
| _children.remove(child); |
| Iterable<WatchNode<T>> grandchildren = child.children; |
| for (WatchNode<T> grandchild in grandchildren) { |
| grandchild.parent = this; |
| _children.add(grandchild); |
| } |
| child._children.clear(); |
| } |
| } |