| OLD | NEW |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library context.directory.manager; | 5 library context.directory.manager; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 import 'dart:collection'; | 8 import 'dart:collection'; |
| 9 | 9 |
| 10 import 'package:analysis_server/src/package_map_provider.dart'; |
| 10 import 'package:analyzer/file_system/file_system.dart'; | 11 import 'package:analyzer/file_system/file_system.dart'; |
| 11 import 'package:analysis_server/src/package_map_provider.dart'; | |
| 12 import 'package:analyzer/src/generated/engine.dart'; | 12 import 'package:analyzer/src/generated/engine.dart'; |
| 13 import 'package:analyzer/src/generated/source.dart'; | 13 import 'package:analyzer/src/generated/source.dart'; |
| 14 import 'package:watcher/watcher.dart'; | 14 import 'package:watcher/watcher.dart'; |
| 15 | 15 |
| 16 /** | 16 /** |
| 17 * Information tracked by the [ContextDirectoryManager] for each context. | |
| 18 */ | |
| 19 class _ContextDirectoryInfo { | |
| 20 /** | |
| 21 * Stream subscription we are using to watch the context's directory for | |
| 22 * changes. | |
| 23 */ | |
| 24 StreamSubscription<WatchEvent> changeSubscription; | |
| 25 | |
| 26 /** | |
| 27 * Map from full path to the [Source] object, for each source that has been | |
| 28 * added to the context. | |
| 29 */ | |
| 30 Map<String, Source> sources = new HashMap<String, Source>(); | |
| 31 | |
| 32 /** | |
| 33 * Dependencies of the context's package map. If any of these files changes, | |
| 34 * the package map needs to be recomputed. | |
| 35 */ | |
| 36 Set<String> packageMapDependencies; | |
| 37 } | |
| 38 | |
| 39 /** | |
| 40 * Class that maintains a mapping from included/excluded paths to a set of | 17 * Class that maintains a mapping from included/excluded paths to a set of |
| 41 * folders that should correspond to analysis contexts. | 18 * folders that should correspond to analysis contexts. |
| 42 */ | 19 */ |
| 43 abstract class ContextDirectoryManager { | 20 abstract class ContextDirectoryManager { |
| 44 /** | 21 /** |
| 45 * File name of pubspec files. | 22 * File name of pubspec files. |
| 46 */ | 23 */ |
| 47 static const String PUBSPEC_NAME = 'pubspec.yaml'; | 24 static const String PUBSPEC_NAME = 'pubspec.yaml'; |
| 48 | 25 |
| 49 /** | 26 /** |
| (...skipping 10 matching lines...) Expand all Loading... |
| 60 | 37 |
| 61 /** | 38 /** |
| 62 * Provider which is used to determine the mapping from package name to | 39 * Provider which is used to determine the mapping from package name to |
| 63 * package folder. | 40 * package folder. |
| 64 */ | 41 */ |
| 65 final PackageMapProvider packageMapProvider; | 42 final PackageMapProvider packageMapProvider; |
| 66 | 43 |
| 67 ContextDirectoryManager(this.resourceProvider, this.packageMapProvider); | 44 ContextDirectoryManager(this.resourceProvider, this.packageMapProvider); |
| 68 | 45 |
| 69 /** | 46 /** |
| 47 * Called when a new context needs to be created. |
| 48 */ |
| 49 void addContext(Folder folder, Map<String, List<Folder>> packageMap); |
| 50 |
| 51 /** |
| 52 * Called when the set of files associated with a context have changed (or |
| 53 * some of those files have been modified). [changeSet] is the set of |
| 54 * changes that need to be applied to the context. |
| 55 */ |
| 56 void applyChangesToContext(Folder contextFolder, ChangeSet changeSet); |
| 57 |
| 58 /** |
| 59 * Returns `true` if the given absolute [path] is in one of the current |
| 60 * root folders and is not excluded. |
| 61 */ |
| 62 bool isInAnalysisRoot(String path) { |
| 63 // TODO(scheglov) check for excluded paths |
| 64 for (Folder root in _currentDirectoryInfo.keys) { |
| 65 if (root.contains(path)) { |
| 66 return true; |
| 67 } |
| 68 } |
| 69 return false; |
| 70 } |
| 71 |
| 72 /** |
| 73 * Remove the context associated with the given [folder]. |
| 74 */ |
| 75 void removeContext(Folder folder); |
| 76 |
| 77 /** |
| 70 * Change the set of paths which should be used as starting points to | 78 * Change the set of paths which should be used as starting points to |
| 71 * determine the context directories. | 79 * determine the context directories. |
| 72 */ | 80 */ |
| 73 void setRoots(List<String> includedPaths, | 81 void setRoots(List<String> includedPaths, List<String> excludedPaths) { |
| 74 List<String> excludedPaths) { | |
| 75 // included | 82 // included |
| 76 Set<Folder> includedFolders = new HashSet<Folder>(); | 83 Set<Folder> includedFolders = new HashSet<Folder>(); |
| 77 for (int i = 0; i < includedPaths.length; i++) { | 84 for (int i = 0; i < includedPaths.length; i++) { |
| 78 String path = includedPaths[i]; | 85 String path = includedPaths[i]; |
| 79 Resource resource = resourceProvider.getResource(path); | 86 Resource resource = resourceProvider.getResource(path); |
| 80 if (resource is Folder) { | 87 if (resource is Folder) { |
| 81 includedFolders.add(resource); | 88 includedFolders.add(resource); |
| 82 } else { | 89 } else { |
| 83 // TODO(scheglov) implemented separate files analysis | 90 // TODO(scheglov) implemented separate files analysis |
| 84 throw new UnimplementedError( | 91 throw new UnimplementedError( |
| 85 '$path is not a folder. ' | 92 '$path is not a folder. ' |
| 86 'Only support for folder analysis is implemented currently.'); | 93 'Only support for folder analysis is implemented currently.'); |
| 87 } | 94 } |
| 88 } | 95 } |
| 89 // excluded | 96 // excluded |
| 90 // TODO(scheglov) remove when implemented | 97 // TODO(scheglov) remove when implemented |
| 91 if (excludedPaths.isNotEmpty) { | 98 if (excludedPaths.isNotEmpty) { |
| 92 throw new UnimplementedError( | 99 throw new UnimplementedError('Excluded paths are not supported yet'); |
| 93 'Excluded paths are not supported yet'); | |
| 94 } | 100 } |
| 95 Set<Folder> excludedFolders = new HashSet<Folder>(); | 101 Set<Folder> excludedFolders = new HashSet<Folder>(); |
| 96 // diff | 102 // diff |
| 97 Set<Folder> currentFolders = _currentDirectoryInfo.keys.toSet(); | 103 Set<Folder> currentFolders = _currentDirectoryInfo.keys.toSet(); |
| 98 Set<Folder> newFolders = includedFolders.difference(currentFolders); | 104 Set<Folder> newFolders = includedFolders.difference(currentFolders); |
| 99 Set<Folder> oldFolders = currentFolders.difference(includedFolders); | 105 Set<Folder> oldFolders = currentFolders.difference(includedFolders); |
| 100 // destroy old contexts | 106 // destroy old contexts |
| 101 for (Folder folder in oldFolders) { | 107 for (Folder folder in oldFolders) { |
| 102 _destroyContext(folder); | 108 _destroyContext(folder); |
| 103 } | 109 } |
| 104 // create new contexts | 110 // create new contexts |
| 105 for (Folder folder in newFolders) { | 111 for (Folder folder in newFolders) { |
| 106 _createContext(folder); | 112 _createContext(folder); |
| 107 } | 113 } |
| 108 } | 114 } |
| 109 | 115 |
| 110 /** | 116 /** |
| 117 * Called when the package map for a context has changed. |
| 118 */ |
| 119 void updateContextPackageMap(Folder contextFolder, Map<String, |
| 120 List<Folder>> packageMap); |
| 121 |
| 122 /** |
| 111 * Create a new context associated with the given folder. | 123 * Create a new context associated with the given folder. |
| 112 */ | 124 */ |
| 113 void _createContext(Folder folder) { | 125 void _createContext(Folder folder) { |
| 114 _ContextDirectoryInfo info = new _ContextDirectoryInfo(); | 126 _ContextDirectoryInfo info = new _ContextDirectoryInfo(); |
| 115 _currentDirectoryInfo[folder] = info; | 127 _currentDirectoryInfo[folder] = info; |
| 116 info.changeSubscription = folder.changes.listen((WatchEvent event) { | 128 info.changeSubscription = folder.changes.listen((WatchEvent event) { |
| 117 _handleWatchEvent(folder, info, event); | 129 _handleWatchEvent(folder, info, event); |
| 118 }); | 130 }); |
| 119 File pubspecFile = folder.getChild(PUBSPEC_NAME); | 131 File pubspecFile = folder.getChild(PUBSPEC_NAME); |
| 120 PackageMapInfo packageMapInfo = packageMapProvider.computePackageMap(folder)
; | 132 PackageMapInfo packageMapInfo = |
| 133 packageMapProvider.computePackageMap(folder); |
| 121 info.packageMapDependencies = packageMapInfo.dependencies; | 134 info.packageMapDependencies = packageMapInfo.dependencies; |
| 122 // TODO(paulberry): if any of the dependencies is outside of [folder], | 135 // TODO(paulberry): if any of the dependencies is outside of [folder], |
| 123 // we'll need to watch their parent folders as well. | 136 // we'll need to watch their parent folders as well. |
| 124 addContext(folder, packageMapInfo.packageMap); | 137 addContext(folder, packageMapInfo.packageMap); |
| 125 ChangeSet changeSet = new ChangeSet(); | 138 ChangeSet changeSet = new ChangeSet(); |
| 126 _addSourceFiles(changeSet, folder, info); | 139 _addSourceFiles(changeSet, folder, info); |
| 127 applyChangesToContext(folder, changeSet); | 140 applyChangesToContext(folder, changeSet); |
| 128 } | 141 } |
| 129 | 142 |
| 130 /** | 143 /** |
| 131 * Clean up and destroy the context associated with the given folder. | 144 * Clean up and destroy the context associated with the given folder. |
| 132 */ | 145 */ |
| 133 void _destroyContext(Folder folder) { | 146 void _destroyContext(Folder folder) { |
| 134 _currentDirectoryInfo[folder].changeSubscription.cancel(); | 147 _currentDirectoryInfo[folder].changeSubscription.cancel(); |
| 135 _currentDirectoryInfo.remove(folder); | 148 _currentDirectoryInfo.remove(folder); |
| 136 removeContext(folder); | 149 removeContext(folder); |
| 137 } | 150 } |
| 138 | 151 |
| 139 void _handleWatchEvent(Folder folder, _ContextDirectoryInfo info, WatchEvent e
vent) { | 152 void _handleWatchEvent(Folder folder, _ContextDirectoryInfo info, |
| 153 WatchEvent event) { |
| 140 switch (event.type) { | 154 switch (event.type) { |
| 141 case ChangeType.ADD: | 155 case ChangeType.ADD: |
| 142 if (_isInPackagesDir(event.path, folder)) { | 156 if (_isInPackagesDir(event.path, folder)) { |
| 143 // TODO(paulberry): perhaps we should only skip packages dirs if | 157 // TODO(paulberry): perhaps we should only skip packages dirs if |
| 144 // there is a pubspec.yaml? | 158 // there is a pubspec.yaml? |
| 145 break; | 159 break; |
| 146 } | 160 } |
| 147 Resource resource = resourceProvider.getResource(event.path); | 161 Resource resource = resourceProvider.getResource(event.path); |
| 148 // If the file went away and was replaced by a folder before we | 162 // If the file went away and was replaced by a folder before we |
| 149 // had a chance to process the event, resource might be a Folder. In | 163 // had a chance to process the event, resource might be a Folder. In |
| 150 // that case don't add it. | 164 // that case don't add it. |
| 151 if (resource is File) { | 165 if (resource is File) { |
| 152 File file = resource; | 166 File file = resource; |
| 153 if (_shouldFileBeAnalyzed(file)) { | 167 if (_shouldFileBeAnalyzed(file)) { |
| 154 ChangeSet changeSet = new ChangeSet(); | 168 ChangeSet changeSet = new ChangeSet(); |
| 155 Source source = file.createSource(); | 169 Source source = file.createSource(); |
| 156 changeSet.addedSource(source); | 170 changeSet.addedSource(source); |
| 157 applyChangesToContext(folder, changeSet); | 171 applyChangesToContext(folder, changeSet); |
| 158 info.sources[event.path]= source; | 172 info.sources[event.path] = source; |
| 159 } | 173 } |
| 160 } | 174 } |
| 161 break; | 175 break; |
| 162 case ChangeType.REMOVE: | 176 case ChangeType.REMOVE: |
| 163 Source source = info.sources[event.path]; | 177 Source source = info.sources[event.path]; |
| 164 if (source != null) { | 178 if (source != null) { |
| 165 ChangeSet changeSet = new ChangeSet(); | 179 ChangeSet changeSet = new ChangeSet(); |
| 166 changeSet.removedSource(source); | 180 changeSet.removedSource(source); |
| 167 applyChangesToContext(folder, changeSet); | 181 applyChangesToContext(folder, changeSet); |
| 168 info.sources.remove(event.path); | 182 info.sources.remove(event.path); |
| 169 } | 183 } |
| 170 break; | 184 break; |
| 171 case ChangeType.MODIFY: | 185 case ChangeType.MODIFY: |
| 172 Source source = info.sources[event.path]; | 186 Source source = info.sources[event.path]; |
| 173 if (source != null) { | 187 if (source != null) { |
| 174 ChangeSet changeSet = new ChangeSet(); | 188 ChangeSet changeSet = new ChangeSet(); |
| 175 changeSet.changedSource(source); | 189 changeSet.changedSource(source); |
| 176 applyChangesToContext(folder, changeSet); | 190 applyChangesToContext(folder, changeSet); |
| 177 } | 191 } |
| 178 break; | 192 break; |
| 179 } | 193 } |
| 180 | 194 |
| 181 if (info.packageMapDependencies.contains(event.path)) { | 195 if (info.packageMapDependencies.contains(event.path)) { |
| 182 // TODO(paulberry): when computePackageMap is changed into an | 196 // TODO(paulberry): when computePackageMap is changed into an |
| 183 // asynchronous API call, we'll want to suspend analysis for this context | 197 // asynchronous API call, we'll want to suspend analysis for this context |
| 184 // while we're rerunning "pub list", since any analysis we complete while | 198 // while we're rerunning "pub list", since any analysis we complete while |
| 185 // "pub list" is in progress is just going to get thrown away anyhow. | 199 // "pub list" is in progress is just going to get thrown away anyhow. |
| 186 PackageMapInfo packageMapInfo = packageMapProvider.computePackageMap(folde
r); | 200 PackageMapInfo packageMapInfo = |
| 201 packageMapProvider.computePackageMap(folder); |
| 187 info.packageMapDependencies = packageMapInfo.dependencies; | 202 info.packageMapDependencies = packageMapInfo.dependencies; |
| 188 updateContextPackageMap(folder, packageMapInfo.packageMap); | 203 updateContextPackageMap(folder, packageMapInfo.packageMap); |
| 189 } | 204 } |
| 190 } | 205 } |
| 191 | 206 |
| 192 /** | 207 /** |
| 193 * Determine if the path from [folder] to [path] contains a 'packages' | 208 * Determine if the path from [folder] to [path] contains a 'packages' |
| 194 * directory. | 209 * directory. |
| 195 */ | 210 */ |
| 196 bool _isInPackagesDir(String path, Folder folder) { | 211 bool _isInPackagesDir(String path, Folder folder) { |
| 197 String relativePath = resourceProvider.pathContext.relative(path, from: fold
er.path); | 212 String relativePath = |
| 213 resourceProvider.pathContext.relative(path, from: folder.path); |
| 198 List<String> pathParts = resourceProvider.pathContext.split(relativePath); | 214 List<String> pathParts = resourceProvider.pathContext.split(relativePath); |
| 199 for (int i = 0; i < pathParts.length - 1; i++) { | 215 for (int i = 0; i < pathParts.length - 1; i++) { |
| 200 if (pathParts[i] == 'packages') { | 216 if (pathParts[i] == 'packages') { |
| 201 return true; | 217 return true; |
| 202 } | 218 } |
| 203 } | 219 } |
| 204 return false; | 220 return false; |
| 205 } | 221 } |
| 206 | 222 |
| 207 /** | 223 /** |
| 208 * Resursively adds all Dart and HTML files to the [changeSet]. | 224 * Resursively adds all Dart and HTML files to the [changeSet]. |
| 209 */ | 225 */ |
| 210 static void _addSourceFiles(ChangeSet changeSet, Folder folder, _ContextDirect
oryInfo info) { | 226 static void _addSourceFiles(ChangeSet changeSet, Folder folder, |
| 227 _ContextDirectoryInfo info) { |
| 211 List<Resource> children = folder.getChildren(); | 228 List<Resource> children = folder.getChildren(); |
| 212 for (Resource child in children) { | 229 for (Resource child in children) { |
| 213 if (child is File) { | 230 if (child is File) { |
| 214 if (_shouldFileBeAnalyzed(child)) { | 231 if (_shouldFileBeAnalyzed(child)) { |
| 215 Source source = child.createSource(); | 232 Source source = child.createSource(); |
| 216 changeSet.addedSource(source); | 233 changeSet.addedSource(source); |
| 217 info.sources[child.path] = source; | 234 info.sources[child.path] = source; |
| 218 } | 235 } |
| 219 } else if (child is Folder) { | 236 } else if (child is Folder) { |
| 220 if (child.shortName == 'packages') { | 237 if (child.shortName == 'packages') { |
| 221 // TODO(paulberry): perhaps we should only skip packages dirs if | 238 // TODO(paulberry): perhaps we should only skip packages dirs if |
| 222 // there is a pubspec.yaml? | 239 // there is a pubspec.yaml? |
| 223 continue; | 240 continue; |
| 224 } | 241 } |
| 225 _addSourceFiles(changeSet, child, info); | 242 _addSourceFiles(changeSet, child, info); |
| 226 } | 243 } |
| 227 } | 244 } |
| 228 } | 245 } |
| 229 | 246 |
| 230 static bool _shouldFileBeAnalyzed(File file) { | 247 static bool _shouldFileBeAnalyzed(File file) { |
| 231 if (!(AnalysisEngine.isDartFileName(file.path) | 248 if (!(AnalysisEngine.isDartFileName(file.path) || |
| 232 || AnalysisEngine.isHtmlFileName(file.path))) { | 249 AnalysisEngine.isHtmlFileName(file.path))) { |
| 233 return false; | 250 return false; |
| 234 } | 251 } |
| 235 // Emacs creates dummy links to track the fact that a file is open for | 252 // Emacs creates dummy links to track the fact that a file is open for |
| 236 // editing and has unsaved changes (e.g. having unsaved changes to | 253 // editing and has unsaved changes (e.g. having unsaved changes to |
| 237 // 'foo.dart' causes a link '.#foo.dart' to be created, which points to the | 254 // 'foo.dart' causes a link '.#foo.dart' to be created, which points to the |
| 238 // non-existent file 'username@hostname.pid'. To avoid these dummy links | 255 // non-existent file 'username@hostname.pid'. To avoid these dummy links |
| 239 // causing the analyzer to thrash, just ignore links to non-existent files. | 256 // causing the analyzer to thrash, just ignore links to non-existent files. |
| 240 return file.exists; | 257 return file.exists; |
| 241 } | 258 } |
| 259 } |
| 260 |
| 261 /** |
| 262 * Information tracked by the [ContextDirectoryManager] for each context. |
| 263 */ |
| 264 class _ContextDirectoryInfo { |
| 265 /** |
| 266 * Stream subscription we are using to watch the context's directory for |
| 267 * changes. |
| 268 */ |
| 269 StreamSubscription<WatchEvent> changeSubscription; |
| 242 | 270 |
| 243 /** | 271 /** |
| 244 * Returns `true` if the given absolute [path] is in one of the current | 272 * Map from full path to the [Source] object, for each source that has been |
| 245 * root folders and is not excluded. | 273 * added to the context. |
| 246 */ | 274 */ |
| 247 bool isInAnalysisRoot(String path) { | 275 Map<String, Source> sources = new HashMap<String, Source>(); |
| 248 // TODO(scheglov) check for excluded paths | |
| 249 for (Folder root in _currentDirectoryInfo.keys) { | |
| 250 if (root.contains(path)) { | |
| 251 return true; | |
| 252 } | |
| 253 } | |
| 254 return false; | |
| 255 } | |
| 256 | 276 |
| 257 /** | 277 /** |
| 258 * Called when a new context needs to be created. | 278 * Dependencies of the context's package map. |
| 279 * If any of these files changes, the package map needs to be recomputed. |
| 259 */ | 280 */ |
| 260 void addContext(Folder folder, Map<String, List<Folder>> packageMap); | 281 Set<String> packageMapDependencies; |
| 261 | |
| 262 /** | |
| 263 * Called when the set of files associated with a context have changed (or | |
| 264 * some of those files have been modified). [changeSet] is the set of | |
| 265 * changes that need to be applied to the context. | |
| 266 */ | |
| 267 void applyChangesToContext(Folder contextFolder, ChangeSet changeSet); | |
| 268 | |
| 269 /** | |
| 270 * Remove the context associated with the given [folder]. | |
| 271 */ | |
| 272 void removeContext(Folder folder); | |
| 273 | |
| 274 /** | |
| 275 * Called when the package map for a context has changed. | |
| 276 */ | |
| 277 void updateContextPackageMap(Folder contextFolder, | |
| 278 Map<String, List<Folder>> packageMap); | |
| 279 } | 282 } |
| OLD | NEW |