private IDependency?FindDependency(GraphNode inputGraphNode, out DependenciesSnapshot?snapshot) { string?projectPath = inputGraphNode.Id.GetValue(CodeGraphNodeIdName.Assembly); if (string.IsNullOrWhiteSpace(projectPath)) { snapshot = null; return(null); } string projectFolder = Path.GetDirectoryName(projectPath); if (projectFolder == null) { snapshot = null; return(null); } string?id = inputGraphNode.GetValue <string>(DependenciesGraphSchema.DependencyIdProperty); bool topLevel; if (id == null) { // this is top level node and it contains full path id = inputGraphNode.Id.GetValue(CodeGraphNodeIdName.File); if (id == null) { // No full path, so this must be a node generated by a different provider. snapshot = null; return(null); } if (id.StartsWith(projectFolder, StringComparison.OrdinalIgnoreCase)) { int startIndex = projectFolder.Length; // Trim backslashes (without allocating) while (startIndex < id.Length && id[startIndex] == '\\') { startIndex++; } id = id.Substring(startIndex); } topLevel = true; } else { topLevel = false; } snapshot = AggregateSnapshotProvider.GetSnapshot(projectPath !); return(snapshot?.FindDependency(id, topLevel)); }
/// <summary> /// Gets a value indicating whether deleting a given set of items from the project, and optionally from disk, /// would be allowed. /// Note: CanRemove can be called several times since there two types of remove operations: /// - Remove is a command that can remove project tree items form the tree/project but not from disk. /// For that command requests deleteOptions has DeleteOptions.None flag. /// - Delete is a command that can remove project tree items and form project and from disk. /// For this command requests deleteOptions has DeleteOptions.DeleteFromStorage flag. /// We can potentially support only Remove command here, since we don't remove Dependencies form disk, /// thus we return false when DeleteOptions.DeleteFromStorage is provided. /// </summary> /// <param name="nodes">The nodes that should be deleted.</param> /// <param name="deleteOptions"> /// A value indicating whether the items should be deleted from disk as well as from the project file. /// </param> public override bool CanRemove(IImmutableSet <IProjectTree> nodes, DeleteOptions deleteOptions = DeleteOptions.None) { if (deleteOptions.HasFlag(DeleteOptions.DeleteFromStorage)) { return(false); } DependenciesSnapshot snapshot = _dependenciesSnapshotProvider.CurrentSnapshot; if (snapshot == null) { return(false); } foreach (IProjectTree node in nodes) { if (!node.Flags.Contains(DependencyTreeFlags.SupportsRemove)) { return(false); } string filePath = UnconfiguredProject.MakeRelative(node.FilePath); if (string.IsNullOrEmpty(filePath)) { continue; } IDependency?dependency = snapshot.FindDependency(filePath, topLevel: true); if (dependency == null || dependency.Implicit) { return(false); } } return(true); }
/// <summary> /// ProjectContextChanged gets fired every time dependencies change for projects across solution. /// <see cref="_expandedGraphContexts"/> contains all nodes that we need to check for potential updates /// in their child dependencies. /// </summary> private void OnSnapshotChanged(object sender, SnapshotChangedEventArgs e) { DependenciesSnapshot snapshot = e.Snapshot; if (snapshot == null || e.Token.IsCancellationRequested) { return; } // _expandedGraphContexts remembers graph expanded or checked so far. // Each context represents one level in the graph, i.e. a node and its first level dependencies // Tracking changes over all expanded contexts ensures that all levels are processed // and updated when there are any changes in nodes data. lock (_lock) { foreach (IGraphContext graphContext in _expandedGraphContexts) { if (e.Token.IsCancellationRequested) { return; } bool anyChanges = false; try { if (HandleChanges(graphContext)) { anyChanges = true; } } finally { // Calling OnCompleted ensures that the changes are reflected in UI if (anyChanges) { graphContext.OnCompleted(); } } } } return; bool HandleChanges(IGraphContext graphContext) { bool anyChanges = false; foreach (GraphNode inputGraphNode in graphContext.InputNodes.ToList()) { string?existingDependencyId = inputGraphNode.GetValue <string>(DependenciesGraphSchema.DependencyIdProperty); if (string.IsNullOrEmpty(existingDependencyId)) { continue; } string?nodeProjectPath = inputGraphNode.Id.GetValue(CodeGraphNodeIdName.Assembly); if (Strings.IsNullOrEmpty(nodeProjectPath)) { continue; } DependenciesSnapshot?updatedSnapshot = _aggregateSnapshotProvider.GetSnapshot(nodeProjectPath); IDependency?updatedDependency = updatedSnapshot?.FindDependency(existingDependencyId); if (updatedDependency == null) // or updatedSnapshot == null { continue; } IDependenciesGraphViewProvider?viewProvider = _viewProviders .FirstOrDefaultValue((x, d) => x.SupportsDependency(d), updatedDependency); if (viewProvider == null) { continue; } if (!viewProvider.ShouldApplyChanges(nodeProjectPath, snapshot.ProjectPath, updatedDependency)) { continue; } using var scope = new GraphTransactionScope(); if (viewProvider.ApplyChanges( graphContext, nodeProjectPath, updatedDependency, inputGraphNode, updatedSnapshot !.DependenciesByTargetFramework[updatedDependency.TargetFramework])) { anyChanges = true; } scope.Complete(); } return(anyChanges); } }
/// <summary> /// Deletes items from the project, and optionally from disk. /// Note: Delete and Remove commands are handled via IVsHierarchyDeleteHandler3, not by /// IAsyncCommandGroupHandler and first asks us we CanRemove nodes. If yes then RemoveAsync is called. /// We can remove only nodes that are standard and based on project items, i.e. nodes that /// are created by default IProjectDependenciesSubTreeProvider implementations and have /// DependencyNode.GenericDependencyFlags flags and IRule with Context != null, in order to obtain /// node's itemSpec. ItemSpec then used to remove a project item having same Include. /// </summary> /// <param name="nodes">The nodes that should be deleted.</param> /// <param name="deleteOptions">A value indicating whether the items should be deleted from disk as well as /// from the project file. /// </param> /// <exception cref="InvalidOperationException">Thrown when <see cref="IProjectTreeProvider.CanRemove"/> /// would return <c>false</c> for this operation.</exception> public override async Task RemoveAsync(IImmutableSet <IProjectTree> nodes, DeleteOptions deleteOptions = DeleteOptions.None) { if (deleteOptions.HasFlag(DeleteOptions.DeleteFromStorage)) { throw new NotSupportedException(); } // Get the list of shared import nodes. IEnumerable <IProjectTree> sharedImportNodes = nodes.Where(node => node.Flags.Contains(DependencyTreeFlags.SharedProjectFlags)); // Get the list of normal reference Item Nodes (this excludes any shared import nodes). IEnumerable <IProjectTree> referenceItemNodes = nodes.Except(sharedImportNodes); Assumes.NotNull(ActiveConfiguredProject); await _projectAccessor.OpenProjectForWriteAsync(ActiveConfiguredProject, project => { // Handle the removal of normal reference Item Nodes (this excludes any shared import nodes). foreach (IProjectTree node in referenceItemNodes) { if (node.BrowseObjectProperties?.Context == null) { // if node does not have an IRule with valid ProjectPropertiesContext we can not // get its itemsSpec. If nodes provided by custom IProjectDependenciesSubTreeProvider // implementation, and have some custom IRule without context, it is not a problem, // since they would not have DependencyNode.GenericDependencyFlags and we would not // end up here, since CanRemove would return false and Remove command would not show // up for those nodes. continue; } IProjectPropertiesContext nodeItemContext = node.BrowseObjectProperties.Context; ProjectItem?unresolvedReferenceItem = project.GetItemsByEvaluatedInclude(nodeItemContext.ItemName) .FirstOrDefault( (item, t) => string.Equals(item.ItemType, t, StringComparisons.ItemTypes), nodeItemContext.ItemType); Report.IfNot(unresolvedReferenceItem != null, "Cannot find reference to remove."); if (unresolvedReferenceItem != null) { project.RemoveItem(unresolvedReferenceItem); } } DependenciesSnapshot snapshot = _dependenciesSnapshotProvider.CurrentSnapshot; Requires.NotNull(snapshot, nameof(snapshot)); if (snapshot == null) { return; } // Handle the removal of shared import nodes. ProjectRootElement projectXml = project.Xml; foreach (IProjectTree sharedImportNode in sharedImportNodes) { string sharedFilePath = UnconfiguredProject.MakeRelative(sharedImportNode.FilePath); if (string.IsNullOrEmpty(sharedFilePath)) { continue; } IDependency?sharedProjectDependency = snapshot.FindDependency(sharedFilePath, topLevel: true); if (sharedProjectDependency != null) { sharedFilePath = sharedProjectDependency.Path; } // Find the import that is included in the evaluation of the specified ConfiguredProject that // imports the project file whose full path matches the specified one. IEnumerable <ResolvedImport> matchingImports = from import in project.Imports where import.ImportingElement.ContainingProject == projectXml && PathHelper.IsSamePath(import.ImportedProject.FullPath, sharedFilePath) select import; foreach (ResolvedImport importToRemove in matchingImports) { ProjectImportElement importingElementToRemove = importToRemove.ImportingElement; Report.IfNot(importingElementToRemove != null, "Cannot find shared project reference to remove."); if (importingElementToRemove != null) { importingElementToRemove.Parent.RemoveChild(importingElementToRemove); } } } }); }