| // 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(); | |
| } | |
| } |