Skip to content

Commit

Permalink
Fix #20: Show a dependency tree of all transient packages.
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-englert committed Oct 14, 2023
1 parent 16ee808 commit e9be321
Show file tree
Hide file tree
Showing 17 changed files with 353 additions and 80 deletions.
3 changes: 1 addition & 2 deletions src/Models/Package.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
using NuGet.Versioning;

namespace NuGetMonitor.Models
{
Expand Down
5 changes: 2 additions & 3 deletions src/Models/TransitiveDependencies.cs
Original file line number Diff line number Diff line change
@@ -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<PackageInfo, HashSet<PackageInfo>> ParentsByChild);
internal sealed record TransitiveDependencies(string ProjectName, NuGetFramework TargetFramework, IReadOnlyDictionary<PackageInfo, HashSet<PackageInfo>> ParentsByChild);
68 changes: 68 additions & 0 deletions src/NuGetMonitorCommands.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
10 changes: 6 additions & 4 deletions src/NuGetMonitorPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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";
Expand All @@ -27,6 +29,6 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke

MonitorService.CheckForUpdates();

await NuGetMonitorCommand.InitializeAsync(this);
await NuGetMonitorCommands.InitializeAsync(this);
}
}
8 changes: 8 additions & 0 deletions src/NuGetMonitorPackage.vsct
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
<ButtonText>NuGet Monitor</ButtonText>
</Strings>
</Button>
<Button guid="guidNuGetMonitorPackageCmdSet" id="DependencyTreeCommandId" priority="0x0100" type="Button">
<Parent guid="guidSHLMainMenu" id="IDG_VS_WNDO_OTRWNDWS1"/>
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>NuGet Dependency Tree</ButtonText>
</Strings>
</Button>
</Buttons>

<Bitmaps>
Expand All @@ -41,6 +48,7 @@
<GuidSymbol name="guidNuGetMonitorPackageCmdSet" value="{df4cd5dd-21c1-4666-8b25-bffe33b47ac1}">
<IDSymbol name="cmdidMyToolsMenuGroup" value="0x1020" />
<IDSymbol name="NuGetMonitorCommandId" value="0x0100" />
<IDSymbol name="DependencyTreeCommandId" value="0x0101" />
</GuidSymbol>

<GuidSymbol name="guidImages" value="{8bddfaf8-b480-4c22-893c-33b486c4d4f0}" >
Expand Down
4 changes: 2 additions & 2 deletions src/Options/General.cs → src/Options/GeneralOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
namespace NuGetMonitor.Options;

[ComVisible(true)]
public class GeneralOptions : BaseOptionPage<General> { }
public class GeneralOptionsPage : BaseOptionPage<GeneralOptions> { }

public sealed class General : BaseOptionModel<General>
public sealed class GeneralOptions : BaseOptionModel<GeneralOptions>
{
[Category("Notifications")]
[DisplayName("Show transitive packages issues")]
Expand Down
19 changes: 8 additions & 11 deletions src/Services/InfoBarService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using NuGet.Versioning;
using NuGetMonitor.Models;
using NuGetMonitor.Options;
using NuGetMonitor.View;
using TomsToolbox.Essentials;

namespace NuGetMonitor.Services;
Expand Down Expand Up @@ -46,10 +45,8 @@ public static void ShowTopLevelPackageIssues(IEnumerable<PackageReferenceInfo> t

public static void ShowTransitivePackageIssues(ICollection<TransitiveDependencies> transitiveDependencies)
{
if (!General.Instance.ShowTransitivePackagesIssues)
{
if (!GeneralOptions.Instance.ShowTransitivePackagesIssues)
return;
}

var transitivePackages = transitiveDependencies
.SelectMany(dependency => dependency.ParentsByChild.Keys)
Expand Down Expand Up @@ -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> transitiveDependencies:
Expand All @@ -134,7 +131,7 @@ private static void PrintDependencyTree(IEnumerable<TransitiveDependencies> depe

foreach (var dependency in dependencies)
{
var (project, targetFramework, packages) = dependency;
var (projectName, targetFramework, packages) = dependency;

var vulnerablePackages = packages
.Select(item => item.Key)
Expand All @@ -144,14 +141,14 @@ private static void PrintDependencyTree(IEnumerable<TransitiveDependencies> 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();
Expand All @@ -162,7 +159,7 @@ private static void PrintDependencyTree(IEnumerable<TransitiveDependencies> depe
ShowInfoBar("Dependency tree copied to clipboard", TimeSpan.FromSeconds(10));
}

private static void PrintDependencyTree(StringBuilder text, PackageInfo package, IReadOnlyDictionary<PackageInfo, HashSet<PackageInfo>> dependencyTree, int nesting)
private static void PrintDependencyTree(StringBuilder text, PackageInfo package, IReadOnlyDictionary<PackageInfo, HashSet<PackageInfo>> parentsByChild, int nesting)
{
var indent = new string(' ', nesting * 4);

Expand All @@ -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);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/Services/NuGetService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.IO;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.Packaging;
Expand Down Expand Up @@ -133,7 +134,7 @@ public static async Task<ICollection<TransitiveDependencies>> 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));
}
}

Expand Down
84 changes: 84 additions & 0 deletions src/View/DependencyTree/DependencyTreeControl.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<UserControl x:Class="NuGetMonitor.View.DependencyTree.DependencyTreeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NuGetMonitor.View.DependencyTree"
xmlns:toms="urn:TomsToolbox"
xmlns:styles="urn:TomsToolbox.Wpf.Styles"
xmlns:imaging="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.Imaging"
xmlns:imageCatalog="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.ImageCatalog"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.DataContext>
<local:DependencyTreeViewModel />
</UserControl.DataContext>
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/NuGetMonitor;component/Resources/VSColorScheme.xaml" />
</ResourceDictionary.MergedDictionaries>
<Thickness x:Key="NodeMargin">4</Thickness>
<HierarchicalDataTemplate x:Key="NodeTemplate"
DataType="{x:Type local:ChildNode}"
ItemsSource="{Binding Children}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
<TextBlock x:Name="TextBlock" Margin="{StaticResource NodeMargin}">
<Run Text="{Binding PackageIdentity, Mode=OneWay}" />
<Run Text="{Binding Issues, Mode=OneWay}" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" />
</TextBlock>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding HasChildren}" Value="False">
<Setter TargetName="TextBlock" Property="FontWeight" Value="Bold" />
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<Grid FocusManager.FocusedElement="{Binding ElementName=TreeView}">
<DockPanel>
<ToolBar DockPanel.Dock="Top"
Style="{DynamicResource {x:Static styles:ResourceKeys.ToolBarStyle}}">
<Button Command="{Binding RefreshCommand}" ToolTip="Refresh">
<imaging:CrispImage Width="16" Height="16" Moniker="{x:Static imageCatalog:KnownMonikers.Refresh}" />
</Button>
</ToolBar>
<TreeView x:Name="TreeView"
BorderThickness="0 1 0 0"
ItemsSource="{Binding TransitivePackages}">
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:RootNode}"
ItemsSource="{Binding Children}"
ItemTemplate="{StaticResource NodeTemplate}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="False" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
<TextBlock Margin="{StaticResource NodeMargin}" FontSize="14">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} [{1}]">
<MultiBinding.Bindings>
<Binding Path="ProjectName" />
<Binding Path="TargetFramework" />
</MultiBinding.Bindings>
</MultiBinding>
</TextBlock.Text>
<!--<Run Text="{Binding ProjectName, Mode=OneWay}" /><Run Text=" [" /><Run Text="{Binding TargetFramework, Mode=OneWay}" /><Run Text="]" />-->
</TextBlock>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</DockPanel>
<toms:LoadingIndicator IsActive="{Binding IsLoading}" Header="Loading..." d:IsHidden="True" />
</Grid>
</UserControl>
13 changes: 13 additions & 0 deletions src/View/DependencyTree/DependencyTreeControl.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace NuGetMonitor.View.DependencyTree
{
/// <summary>
/// Interaction logic for DependencyTreeControl.xaml
/// </summary>
public partial class DependencyTreeControl
{
public DependencyTreeControl()
{
InitializeComponent();
}
}
}
17 changes: 17 additions & 0 deletions src/View/DependencyTree/DependencyTreeToolWindow.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Initializes a new instance of the <see cref="DependencyTreeToolWindow"/> class.
/// </summary>
public DependencyTreeToolWindow() : base(null)
{
Caption = "Package Dependency Tree";
Content = new DependencyTreeControl();
}
}
Loading

0 comments on commit e9be321

Please sign in to comment.