diff --git a/src/Models/Package.cs b/src/Models/Package.cs index efb323e..0a503f0 100644 --- a/src/Models/Package.cs +++ b/src/Models/Package.cs @@ -1,5 +1,4 @@ -using NuGet.Protocol.Core.Types; -using NuGet.Versioning; +using NuGet.Versioning; namespace NuGetMonitor.Models { diff --git a/src/Models/TransitiveDependencies.cs b/src/Models/TransitiveDependencies.cs index ba2d885..a7af346 100644 --- a/src/Models/TransitiveDependencies.cs +++ b/src/Models/TransitiveDependencies.cs @@ -1,6 +1,5 @@ -using Microsoft.Build.Evaluation; -using NuGet.Frameworks; +using NuGet.Frameworks; namespace NuGetMonitor.Models; -internal sealed record TransitiveDependencies(Project Project, NuGetFramework TargetFramework, IReadOnlyDictionary> ParentsByChild); \ No newline at end of file +internal sealed record TransitiveDependencies(string ProjectName, NuGetFramework TargetFramework, IReadOnlyDictionary> ParentsByChild); \ No newline at end of file diff --git a/src/NuGetMonitorCommands.cs b/src/NuGetMonitorCommands.cs new file mode 100644 index 0000000..8aa51f0 --- /dev/null +++ b/src/NuGetMonitorCommands.cs @@ -0,0 +1,68 @@ +using System.ComponentModel.Design; +using Microsoft.VisualStudio.Shell; +using NuGetMonitor.View.Monitor; + +namespace NuGetMonitor; + +internal sealed class NuGetMonitorCommands +{ + private const int _monitorCommandId = 0x0100; + private const int _dependencyTreeCommandId = 0x0101; + + private static readonly Guid _commandSet = new("df4cd5dd-21c1-4666-8b25-bffe33b47ac1"); + + private readonly AsyncPackage _package; + + private NuGetMonitorCommands(AsyncPackage package, OleMenuCommandService commandService) + { + _package = package ?? throw new ArgumentNullException(nameof(package)); + commandService = commandService ?? throw new ArgumentNullException(nameof(commandService)); + + commandService.AddCommand(new MenuCommand(ExecuteMonitorCommand, new CommandID(_commandSet, _monitorCommandId))); + commandService.AddCommand(new MenuCommand(ExecuteDependencyTreeCommand, new CommandID(_commandSet, _dependencyTreeCommandId))); + } + + public static NuGetMonitorCommands? Instance + { + get; + private set; + } + + public static async Task InitializeAsync(AsyncPackage package) + { + await JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken); + + var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)).ConfigureAwait(true) as OleMenuCommandService ?? throw new InvalidOperationException("Failed to get menu command service"); + + Instance = new NuGetMonitorCommands(package, commandService); + } + + public void ShowMonitorToolWindow() + { + _package.JoinableTaskFactory.RunAsync(async delegate + { + var window = await _package.ShowToolWindowAsync(typeof(NuGetMonitorToolWindow), 0, true, _package.DisposalToken); + if (window?.Frame == null) + throw new NotSupportedException("Cannot create tool window"); + }).FireAndForget(); + } + + private void ExecuteMonitorCommand(object sender, EventArgs e) + { + ShowMonitorToolWindow(); + } + public void ShowDependencyTreeToolWindow() + { + _package.JoinableTaskFactory.RunAsync(async delegate + { + var window = await _package.ShowToolWindowAsync(typeof(DependencyTreeToolWindow), 0, true, _package.DisposalToken); + if (window?.Frame == null) + throw new NotSupportedException("Cannot create tool window"); + }).FireAndForget(); + } + + private void ExecuteDependencyTreeCommand(object sender, EventArgs e) + { + ShowDependencyTreeToolWindow(); + } +} \ No newline at end of file diff --git a/src/NuGetMonitorPackage.cs b/src/NuGetMonitorPackage.cs index c08b3cd..51ff4b5 100644 --- a/src/NuGetMonitorPackage.cs +++ b/src/NuGetMonitorPackage.cs @@ -4,7 +4,8 @@ using Microsoft.VisualStudio.Shell; using NuGetMonitor.Options; using NuGetMonitor.Services; -using NuGetMonitor.View; +using NuGetMonitor.View.DependencyTree; +using NuGetMonitor.View.Monitor; namespace NuGetMonitor; @@ -13,8 +14,9 @@ namespace NuGetMonitor; [ProvideAutoLoad(VSConstants.UICONTEXT.SolutionExistsAndFullyLoaded_string, PackageAutoLoadFlags.BackgroundLoad)] [ProvideMenuResource("Menus.ctmenu", 1)] [ProvideToolWindow(typeof(NuGetMonitorToolWindow))] -[ProvideOptionPage(typeof(GeneralOptions), "NuGet Monitor", "General", 0, 0, true)] -[ProvideProfile(typeof(GeneralOptions), "NuGet Monitor", "General", 0, 0, true)] +[ProvideToolWindow(typeof(DependencyTreeToolWindow))] +[ProvideOptionPage(typeof(GeneralOptionsPage), "NuGet Monitor", "General", 0, 0, true)] +[ProvideProfile(typeof(GeneralOptionsPage), "NuGet Monitor", "General", 0, 0, true)] public sealed class NuGetMonitorPackage : ToolkitPackage { public const string PackageGuidString = "38279e01-6b27-4a29-9221-c4ea8748f16e"; @@ -27,6 +29,6 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke MonitorService.CheckForUpdates(); - await NuGetMonitorCommand.InitializeAsync(this); + await NuGetMonitorCommands.InitializeAsync(this); } } \ No newline at end of file diff --git a/src/NuGetMonitorPackage.vsct b/src/NuGetMonitorPackage.vsct index eacec64..57346ea 100644 --- a/src/NuGetMonitorPackage.vsct +++ b/src/NuGetMonitorPackage.vsct @@ -26,6 +26,13 @@ NuGet Monitor + @@ -41,6 +48,7 @@ + diff --git a/src/Options/General.cs b/src/Options/GeneralOptions.cs similarity index 75% rename from src/Options/General.cs rename to src/Options/GeneralOptions.cs index c8c6895..abafb30 100644 --- a/src/Options/General.cs +++ b/src/Options/GeneralOptions.cs @@ -5,9 +5,9 @@ namespace NuGetMonitor.Options; [ComVisible(true)] -public class GeneralOptions : BaseOptionPage { } +public class GeneralOptionsPage : BaseOptionPage { } -public sealed class General : BaseOptionModel +public sealed class GeneralOptions : BaseOptionModel { [Category("Notifications")] [DisplayName("Show transitive packages issues")] diff --git a/src/Services/InfoBarService.cs b/src/Services/InfoBarService.cs index c2861ed..177c2ed 100644 --- a/src/Services/InfoBarService.cs +++ b/src/Services/InfoBarService.cs @@ -8,7 +8,6 @@ using NuGet.Versioning; using NuGetMonitor.Models; using NuGetMonitor.Options; -using NuGetMonitor.View; using TomsToolbox.Essentials; namespace NuGetMonitor.Services; @@ -46,10 +45,8 @@ public static void ShowTopLevelPackageIssues(IEnumerable t public static void ShowTransitivePackageIssues(ICollection transitiveDependencies) { - if (!General.Instance.ShowTransitivePackagesIssues) - { + if (!GeneralOptions.Instance.ShowTransitivePackagesIssues) return; - } var transitivePackages = transitiveDependencies .SelectMany(dependency => dependency.ParentsByChild.Keys) @@ -117,7 +114,7 @@ private static void InfoBar_ActionItemClicked(object sender, InfoBarActionItemEv switch (e.ActionItem.ActionContext) { case Actions.Manage: - NuGetMonitorCommand.Instance?.ShowToolWindow(); + NuGetMonitorCommands.Instance?.ShowMonitorToolWindow(); break; case ICollection transitiveDependencies: @@ -134,7 +131,7 @@ private static void PrintDependencyTree(IEnumerable depe foreach (var dependency in dependencies) { - var (project, targetFramework, packages) = dependency; + var (projectName, targetFramework, packages) = dependency; var vulnerablePackages = packages .Select(item => item.Key) @@ -144,14 +141,14 @@ private static void PrintDependencyTree(IEnumerable depe if (vulnerablePackages.Length == 0) continue; - var header = $"{Path.GetFileName(project.FullPath)}, {targetFramework}"; + var header = $"{projectName}, {targetFramework}"; text.AppendLine(header) .AppendLine(new string('-', header.Length)); foreach (var vulnerablePackage in vulnerablePackages) { - PrintDependencyTree(text, vulnerablePackage, dependency.ParentsByChild, 0); + PrintDependencyTree(text, vulnerablePackage, packages, 0); } text.AppendLine().AppendLine(); @@ -162,7 +159,7 @@ private static void PrintDependencyTree(IEnumerable depe ShowInfoBar("Dependency tree copied to clipboard", TimeSpan.FromSeconds(10)); } - private static void PrintDependencyTree(StringBuilder text, PackageInfo package, IReadOnlyDictionary> dependencyTree, int nesting) + private static void PrintDependencyTree(StringBuilder text, PackageInfo package, IReadOnlyDictionary> parentsByChild, int nesting) { var indent = new string(' ', nesting * 4); @@ -186,12 +183,12 @@ private static void PrintDependencyTree(StringBuilder text, PackageInfo package, text.AppendLine(); - if (!dependencyTree.TryGetValue(package, out var dependsOn)) + if (!parentsByChild.TryGetValue(package, out var dependsOn)) return; foreach (var item in dependsOn) { - PrintDependencyTree(text, item, dependencyTree, nesting + 1); + PrintDependencyTree(text, item, parentsByChild, nesting + 1); } } diff --git a/src/Services/NuGetService.cs b/src/Services/NuGetService.cs index 6235d16..0eb90a8 100644 --- a/src/Services/NuGetService.cs +++ b/src/Services/NuGetService.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Caching.Memory; +using Microsoft.IO; using NuGet.Common; using NuGet.Frameworks; using NuGet.Packaging; @@ -133,7 +134,7 @@ public static async Task> GetTransitivePacka .Where(item => transitivePackageIdentities.Contains(item.Key.PackageIdentity)) .ToDictionary(); - results.Add(new TransitiveDependencies(project, targetFramework, parentsByChild)); + results.Add(new TransitiveDependencies(Path.GetFileName(project.FullPath), targetFramework, parentsByChild)); } } diff --git a/src/View/DependencyTree/DependencyTreeControl.xaml b/src/View/DependencyTree/DependencyTreeControl.xaml new file mode 100644 index 0000000..d659ddc --- /dev/null +++ b/src/View/DependencyTree/DependencyTreeControl.xaml @@ -0,0 +1,84 @@ + + + + + + + + + + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/View/DependencyTree/DependencyTreeControl.xaml.cs b/src/View/DependencyTree/DependencyTreeControl.xaml.cs new file mode 100644 index 0000000..65d1726 --- /dev/null +++ b/src/View/DependencyTree/DependencyTreeControl.xaml.cs @@ -0,0 +1,13 @@ +namespace NuGetMonitor.View.DependencyTree +{ + /// + /// Interaction logic for DependencyTreeControl.xaml + /// + public partial class DependencyTreeControl + { + public DependencyTreeControl() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/View/DependencyTree/DependencyTreeToolWindow.cs b/src/View/DependencyTree/DependencyTreeToolWindow.cs new file mode 100644 index 0000000..292c6a8 --- /dev/null +++ b/src/View/DependencyTree/DependencyTreeToolWindow.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; +using Microsoft.VisualStudio.Shell; + +namespace NuGetMonitor.View.DependencyTree; + +[Guid("C82FB9BC-D58C-48CA-95EC-40905527089F")] +public sealed class DependencyTreeToolWindow : ToolWindowPane +{ + /// + /// Initializes a new instance of the class. + /// + public DependencyTreeToolWindow() : base(null) + { + Caption = "Package Dependency Tree"; + Content = new DependencyTreeControl(); + } +} \ No newline at end of file diff --git a/src/View/DependencyTree/DependencyTreeViewModel.cs b/src/View/DependencyTree/DependencyTreeViewModel.cs new file mode 100644 index 0000000..0b48ffe --- /dev/null +++ b/src/View/DependencyTree/DependencyTreeViewModel.cs @@ -0,0 +1,127 @@ +using Community.VisualStudio.Toolkit; +using NuGetMonitor.Services; +using System.ComponentModel; +using System.Windows.Input; +using Microsoft.VisualStudio.Shell; +using NuGet.Frameworks; +using NuGetMonitor.Models; +using TomsToolbox.Wpf; +using NuGet.Packaging.Core; + +namespace NuGetMonitor.View.DependencyTree; + +internal sealed partial class ChildNode : INotifyPropertyChanged +{ + private readonly PackageInfo _packageInfo; + private readonly IReadOnlyDictionary> _parentsByChild; + private readonly HashSet? _dependsOn; + + public ChildNode(PackageInfo packageInfo, IReadOnlyDictionary> parentsByChild) + { + _packageInfo = packageInfo; + _parentsByChild = parentsByChild; + + parentsByChild.TryGetValue(_packageInfo, out _dependsOn); + } + + public PackageIdentity PackageIdentity => _packageInfo.PackageIdentity; + + public IEnumerable? Children => _dependsOn? + .OrderBy(item => item.PackageIdentity) + .Select(item => new ChildNode(item, _parentsByChild)); + + public bool HasChildren => _dependsOn != null; + + public string Issues => GetIssues(); + + private string GetIssues() + { + var items = GetIssueItems().ToArray(); + if (items.Length == 0) + return string.Empty; + + return $" [{string.Join(", ", items)}]"; + } + + private IEnumerable GetIssueItems() + { + if (_packageInfo.IsDeprecated) + yield return "Deprecated"; + + if (_packageInfo.IsOutdated) + yield return "Outdated"; + + if (_packageInfo.IsVulnerable) + yield return "Vulnerable"; + } +} + +internal sealed partial class RootNode : INotifyPropertyChanged +{ + private readonly TransitiveDependencies _transitiveDependencies; + + public RootNode(TransitiveDependencies transitiveDependencies) + { + _transitiveDependencies = transitiveDependencies; + } + + public string ProjectName => _transitiveDependencies.ProjectName; + + public NuGetFramework TargetFramework => _transitiveDependencies.TargetFramework; + + public IEnumerable Children => _transitiveDependencies.ParentsByChild + .OrderBy(item => item.Key.PackageIdentity) + .Select(item => new ChildNode(item.Key, _transitiveDependencies.ParentsByChild)); +} + +internal sealed partial class DependencyTreeViewModel : INotifyPropertyChanged +{ + public DependencyTreeViewModel() + { + VS.Events.SolutionEvents.OnAfterOpenSolution += SolutionEvents_OnAfterOpenSolution; + VS.Events.SolutionEvents.OnAfterCloseSolution += SolutionEvents_OnAfterCloseSolution; + Load().FireAndForget(); + } + + public bool IsLoading { get; set; } = true; + + public ICollection? TransitivePackages { get; private set; } + + public ICommand RefreshCommand => new DelegateCommand(() => Load().FireAndForget()); + + public async Task Load() + { + try + { + IsLoading = true; + + var packageReferences = await ProjectService.GetPackageReferences().ConfigureAwait(true); + + var topLevelPackages = await NuGetService.CheckPackageReferences(packageReferences).ConfigureAwait(true); + + if (topLevelPackages.Count == 0) + return; + + var transitivePackages = await NuGetService.GetTransitivePackages(packageReferences, topLevelPackages).ConfigureAwait(true); + + TransitivePackages = transitivePackages + .OrderBy(item => item.ProjectName) + .ThenBy(item => item.TargetFramework.ToString()) + .Select(item => new RootNode(item)).ToArray(); + } + finally + { + IsLoading = false; + } + } + + private void SolutionEvents_OnAfterOpenSolution(Solution? obj) + { + Load().FireAndForget(); + } + + private void SolutionEvents_OnAfterCloseSolution() + { + TransitivePackages = null; + } +} \ No newline at end of file diff --git a/src/View/NuGetMonitorControl.xaml b/src/View/Monitor/NuGetMonitorControl.xaml similarity index 97% rename from src/View/NuGetMonitorControl.xaml rename to src/View/Monitor/NuGetMonitorControl.xaml index 743a72c..48a169b 100644 --- a/src/View/NuGetMonitorControl.xaml +++ b/src/View/Monitor/NuGetMonitorControl.xaml @@ -46,6 +46,9 @@ + @@ -54,6 +57,7 @@ ItemsSource="{Binding Packages}" AutoGenerateColumns="False" IsReadOnly="True" + BorderThickness="0 1 0 0" Style="{DynamicResource {x:Static styles:ResourceKeys.DataGridStyle}}" VerticalContentAlignment="Center" dgx:DataGridFilter.IsAutoFilterEnabled="True" @@ -212,6 +216,6 @@ - + \ No newline at end of file diff --git a/src/View/NuGetMonitorToolWindow.cs b/src/View/Monitor/NuGetMonitorToolWindow.cs similarity index 92% rename from src/View/NuGetMonitorToolWindow.cs rename to src/View/Monitor/NuGetMonitorToolWindow.cs index 5fe62cd..b6794d3 100644 --- a/src/View/NuGetMonitorToolWindow.cs +++ b/src/View/Monitor/NuGetMonitorToolWindow.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using Microsoft.VisualStudio.Shell; -namespace NuGetMonitor.View; +namespace NuGetMonitor.View.Monitor; [Guid("6ce47eec-3296-48f5-9dec-8883a276a7c8")] diff --git a/src/View/NugetMonitorControl.xaml.cs b/src/View/Monitor/NugetMonitorControl.xaml.cs similarity index 100% rename from src/View/NugetMonitorControl.xaml.cs rename to src/View/Monitor/NugetMonitorControl.xaml.cs diff --git a/src/View/NugetMonitorViewModel.cs b/src/View/Monitor/NugetMonitorViewModel.cs similarity index 95% rename from src/View/NugetMonitorViewModel.cs rename to src/View/Monitor/NugetMonitorViewModel.cs index fe44d92..5eea2a3 100644 --- a/src/View/NugetMonitorViewModel.cs +++ b/src/View/Monitor/NugetMonitorViewModel.cs @@ -35,6 +35,8 @@ public NuGetMonitorViewModel() public ICommand RefreshCommand => new DelegateCommand(Refresh); + public static ICommand ShowDependencyTreeCommand => new DelegateCommand(ShowDependencyTree); + public static ICommand ShowNuGetPackageManagerCommand => new DelegateCommand(ShowNuGetPackageManager); public ICommand CopyIssueDetailsCommand => new DelegateCommand(CanCopyIssueDetails, CopyIssueDetails); @@ -88,6 +90,11 @@ private static void ShowNuGetPackageManager() VS.Commands.ExecuteAsync("Tools.ManageNuGetPackagesForSolution").FireAndForget(); } + private static void ShowDependencyTree() + { + NuGetMonitorCommands.Instance?.ShowDependencyTreeToolWindow(); + } + private void Refresh(DataGrid dataGrid) { dataGrid.GetFilter().Clear(); @@ -152,6 +159,8 @@ private static void Update(ICollection packageViewModels) { packageViewModel.ApplySelectedVersion(); } + + ProjectService.ClearCache(); } private bool CanCopyIssueDetails() diff --git a/src/View/NuGetMonitorCommand.cs b/src/View/NuGetMonitorCommand.cs deleted file mode 100644 index 74e21c5..0000000 --- a/src/View/NuGetMonitorCommand.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.ComponentModel.Design; -using Microsoft.VisualStudio.Shell; - -namespace NuGetMonitor.View; - -internal sealed class NuGetMonitorCommand -{ - private const int _commandId = 0x0100; - - private static readonly Guid _commandSet = new("df4cd5dd-21c1-4666-8b25-bffe33b47ac1"); - - private readonly AsyncPackage _package; - - private NuGetMonitorCommand(AsyncPackage package, OleMenuCommandService commandService) - { - _package = package ?? throw new ArgumentNullException(nameof(package)); - commandService = commandService ?? throw new ArgumentNullException(nameof(commandService)); - - var menuCommandId = new CommandID(_commandSet, _commandId); - var menuItem = new MenuCommand(Execute, menuCommandId); - commandService.AddCommand(menuItem); - } - - public static NuGetMonitorCommand? Instance - { - get; - private set; - } - - public static async Task InitializeAsync(AsyncPackage package) - { - await JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken); - - var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)).ConfigureAwait(true) as OleMenuCommandService ?? throw new InvalidOperationException("Failed to get menu command service"); - - Instance = new NuGetMonitorCommand(package, commandService); - } - - public void ShowToolWindow() - { - _package.JoinableTaskFactory.RunAsync(async delegate - { - var window = await _package.ShowToolWindowAsync(typeof(NuGetMonitorToolWindow), 0, true, _package.DisposalToken); - if (null == window || null == window.Frame) - { - throw new NotSupportedException("Cannot create tool window"); - } - }).FireAndForget(); - } - - private void Execute(object sender, EventArgs e) - { - ShowToolWindow(); - } -} \ No newline at end of file