Chromium Code Reviews| Index: pkg/analysis_server/lib/src/watch_manager.dart |
| diff --git a/pkg/analysis_server/lib/src/watch_manager.dart b/pkg/analysis_server/lib/src/watch_manager.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..4dbcc676b6f2a235e04c1681652254ae75806761 |
| --- /dev/null |
| +++ b/pkg/analysis_server/lib/src/watch_manager.dart |
| @@ -0,0 +1,305 @@ |
| +// 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. |
| + |
| +library context.directory.manager; |
| + |
| +import 'dart:async'; |
| +import 'dart:collection'; |
| +import 'dart:core' hide Resource; |
| + |
| +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 differenciate 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]. |
|
Paul Berry
2015/07/19 18:38:34
Documentation should say what happens if the same
Brian Wilkerson
2015/07/19 21:24:19
Done
|
| + */ |
| + 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((WatchEvent event) { |
|
Paul Berry
2015/07/19 18:38:34
How about just:
folderNode.subscription = folder.
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + _handleWatchEvent(event); |
| + }); |
| + // |
| + // 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) { |
| + if (childNode.subscription != null) { |
|
Paul Berry
2015/07/19 18:38:33
Consider adding:
assert(childNode.subscription !=
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + childNode.subscription.cancel(); |
| + childNode.subscription = null; |
| + } |
| + } |
| + } |
| + } |
| + |
| + /** |
| + * Record that we are no longer watching the given [folder] with the given |
| + * [token]. |
| + */ |
|
Paul Berry
2015/07/19 18:38:33
I like the fact that nothing happens if removeFold
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + void removeFolder(Folder folder, T token) { |
| + WatchNode<T> folderNode = _watchedFolders[folder]; |
| + if (folderNode == null) { |
| + return; |
| + } |
| + Set<T> tokens = folderNode.tokens; |
| + tokens.remove(token); |
| + // |
| + // 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 it's children and cancel it's subscription. |
|
Paul Berry
2015/07/19 18:38:33
Nit: both "it's" should be "its".
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + // |
| + if (folderNode.subscription != null) { |
| + for (WatchNode<T> childNode in folderNode.children) { |
| + childNode.subscription = childNode.folder.changes |
|
Paul Berry
2015/07/19 18:38:33
Consider adding "assert(childNode.subscription ==
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + .listen((WatchEvent event) { |
|
Paul Berry
2015/07/19 18:38:33
Again, ".listen(_handleWatchEvent);" should be suf
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + _handleWatchEvent(event); |
| + }); |
| + } |
| + 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>[]; |
| +// void visitNode(WatchNode node) { |
|
Paul Berry
2015/07/19 18:38:34
Was this commented-out code left here by accident?
Brian Wilkerson
2015/07/19 21:24:20
It was missed while cleaning-up.
|
| +// Folder folder = node.folder; |
| +// if (folder.contains(path)) { |
| +// tokens.addAll(_watchedFolders[folder].tokens); |
| +// } |
| +// node.forEachChild(visitNode); |
| +// } |
| +// visitNode(rootFolder.findParent(path)); |
| + WatchNode<T> parent = rootNode.findParent(path); |
|
Paul Berry
2015/07/19 18:38:34
What happens if path is identical to the path to o
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + 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> { |
| + /** |
| + * An empty list of watch nodes. |
| + */ |
| + static List<WatchNode> EMPTY_LIST = <WatchNode>[]; |
|
Paul Berry
2015/07/19 18:38:33
I think this goes away if you take my suggestion b
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + |
| + /** |
| + * 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. |
| + */ |
| + Folder folder; |
|
Paul Berry
2015/07/19 18:38:33
Can this be final?
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + |
| + /** |
| + * The parent of this node. |
| + */ |
| + WatchNode parent; |
| + |
| + /** |
| + * The information for the children of this node. |
| + */ |
| + List<WatchNode> _children = null; |
|
Paul Berry
2015/07/19 18:38:33
It seems like premature optimization to use `null`
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + |
| + /** |
| + * The tokens that were used to register interest in watching this folder. |
| + */ |
| + Set<T> tokens = new HashSet<T>(); |
|
Paul Berry
2015/07/19 18:38:34
This can be final.
Brian Wilkerson
2015/07/19 21:24:19
Done
|
| + |
| + /** |
| + * 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. |
| + */ |
| + List<WatchNode> get children => _children == null ? EMPTY_LIST : _children; |
|
Paul Berry
2015/07/19 18:38:34
Consider changing the return type to Iterable<Watc
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + |
| + /** |
| + * Remove this node from the tree of watched folders. |
| + */ |
| + void delete() { |
| + if (parent != null) { |
| + parent._removeChild(this); |
| + } |
|
Paul Berry
2015/07/19 18:38:34
Consider setting parent to null so the node return
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + } |
| + |
| + /** |
| + * 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 findParent(String filePath) { |
| + if (_children == null) { |
| + return this; |
| + } |
| + for (WatchNode childNode in _children) { |
| + if (childNode.folder.contains(filePath)) { |
| + return childNode.findParent(filePath); |
| + } |
| + } |
| + return this; |
| + } |
| + |
| + @override |
| + String toString() => 'WatchNode (' |
| + 'folder = ${folder == null ? '<root>' : folder.path}, ' |
| + 'tokens = $tokens, ' |
| + 'subscription = ${subscription == null ? 'null' : 'non-null'})'; |
| + |
| + /** |
| + * Invoke the given [function] on each of the children of this node. |
| + */ |
| + void forEachChild(void function(WatchNode<T> node)) { |
|
Paul Berry
2015/07/19 18:38:33
This method is only called from commented-out code
Brian Wilkerson
2015/07/19 21:24:20
Done
|
| + if (_children != null) { |
| + _children.forEach(function); |
| + } |
| + } |
| + |
| + /** |
| + * 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 insert(WatchNode node) { |
| + WatchNode parentNode = findParent(node.folder.path); |
| + parentNode._addChild(node, true); |
| + return parentNode; |
| + } |
| + |
| + /** |
| + * 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 newChild, bool checkChildren) { |
| + if (_children == null) { |
| + _children = <WatchNode>[]; |
| + } else if (checkChildren) { |
| + Folder folder = newChild.folder; |
| + for (int i = _children.length - 1; i >= 0; i--) { |
| + WatchNode 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 child) { |
| + _children.remove(child); |
| + for (WatchNode grandchild in child.children) { |
| + grandchild.parent = this; |
| + _children.add(grandchild); |
| + } |
| + if (_children.isEmpty) { |
| + _children = null; |
| + } |
| + } |
|
Paul Berry
2015/07/19 18:38:33
Consider setting child._children to null so that t
Brian Wilkerson
2015/07/19 21:24:21
I can't because now there's an assumption that _ch
|
| +} |