blob: 8a3fc97d80925956a2f58862b4726f940b612f92 [file] [log] [blame]
// 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();
}
}