diff --git a/src/NuGetMonitor/Options/GeneralOptions.cs b/src/NuGetMonitor/Options/GeneralOptions.cs index 9f9da5e..baee470 100644 --- a/src/NuGetMonitor/Options/GeneralOptions.cs +++ b/src/NuGetMonitor/Options/GeneralOptions.cs @@ -20,5 +20,11 @@ public sealed class GeneralOptions : BaseOptionModel [Description("Show a notification detailing vulnerable transitive packages.")] [DefaultValue(true)] public bool ShowTransitivePackagesIssues { get; set; } = true; + + [Category("Notifications")] + [DisplayName("Open NuGet Package Manager")] + [Description("Open the built-in NuGet Package Manager instead of the NuGet Monitor Package Manager.")] + [DefaultValue(true)] + public bool OpenNuGetPackageManager { get; set; } = false; } diff --git a/src/NuGetMonitor/Services/InfoBarService.cs b/src/NuGetMonitor/Services/InfoBarService.cs index 51fd9d5..8ca2fcc 100644 --- a/src/NuGetMonitor/Services/InfoBarService.cs +++ b/src/NuGetMonitor/Services/InfoBarService.cs @@ -15,202 +15,204 @@ namespace NuGetMonitor.Services; internal static class InfoBarService { - private static readonly List _infoBars = new(); - - private enum Actions - { - Manage - } - - public static void ShowTopLevelPackageIssues(IEnumerable topLevelPackages) - { - var message = string.Join(", ", GetInfoTexts(topLevelPackages).ExceptNullItems()); - - if (string.IsNullOrEmpty(message)) - { - Log("No issues found"); - return; - } - - Log(message); - - var textSpans = new[] - { - new InfoBarTextSpan($"{message}. "), - new InfoBarHyperlink("Manage", Actions.Manage), - new InfoBarTextSpan(" packages.") - }; - - ShowInfoBar(textSpans).FireAndForget(); - } - - public static void ShowTransitivePackageIssues(ICollection transitiveDependencies) - { - if (!GeneralOptions.Instance.ShowTransitivePackagesIssues) - return; - - var transitivePackages = transitiveDependencies - .SelectMany(dependency => dependency.ParentsByChild.Keys) - .Distinct() - .ToArray(); + private static readonly List _infoBars = new(); + + private enum Actions + { + Manage + } + + public static void ShowTopLevelPackageIssues(IEnumerable topLevelPackages) + { + var message = string.Join(", ", GetInfoTexts(topLevelPackages).ExceptNullItems()); + + if (string.IsNullOrEmpty(message)) + { + Log("No issues found"); + return; + } + + Log(message); + + var textSpans = new[] + { + new InfoBarTextSpan($"{message}. "), + new InfoBarHyperlink("Manage", Actions.Manage), + new InfoBarTextSpan(" packages.") + }; + + ShowInfoBar(textSpans).FireAndForget(); + } + + public static void ShowTransitivePackageIssues(ICollection transitiveDependencies) + { + if (!GeneralOptions.Instance.ShowTransitivePackagesIssues) + return; + + var transitivePackages = transitiveDependencies + .SelectMany(dependency => dependency.ParentsByChild.Keys) + .Distinct() + .ToArray(); + + Log($"{transitivePackages.Length} transitive packages found"); + + var vulnerablePackages = transitivePackages.Where(item => item.IsVulnerable && item.VulnerabilityMitigation.IsNullOrEmpty()).ToArray(); + + if (vulnerablePackages.Length <= 0) + { + Log("No issues found"); + return; + } + + var packageInfo = string.Join("\r\n- ", vulnerablePackages.Select(package => package.PackageIdentity)); + var message = $"{vulnerablePackages.CountedDescription("vulnerability")} in transitive dependencies:\r\n- {packageInfo}\r\n"; + + Log(message); - Log($"{transitivePackages.Length} transitive packages found"); - - var vulnerablePackages = transitivePackages.Where(item => item.IsVulnerable && item.VulnerabilityMitigation.IsNullOrEmpty()).ToArray(); + var textSpans = new[] + { + new InfoBarTextSpan(message), + new InfoBarHyperlink("Copy details", transitiveDependencies) + }; - if (vulnerablePackages.Length <= 0) - { - Log("No issues found"); - return; - } + ShowInfoBar(textSpans).FireAndForget(); + } - var packageInfo = string.Join("\r\n- ", vulnerablePackages.Select(package => package.PackageIdentity)); - var message = $"{vulnerablePackages.CountedDescription("vulnerability")} in transitive dependencies:\r\n- {packageInfo}\r\n"; + private static void ShowInfoBar(string text, TimeSpan? timeOut = default) + { + ShowInfoBar(new[] { new InfoBarTextSpan(text) }, timeOut).FireAndForget(); + } - Log(message); + private static async Task ShowInfoBar(IEnumerable textSpans, TimeSpan? timeOut = default) + { + var model = new InfoBarModel(textSpans, KnownMonikers.NuGet, isCloseButtonVisible: true); + + var infoBar = await VS.InfoBar.CreateAsync(ToolWindowGuids80.SolutionExplorer, model) ?? throw new InvalidOperationException("Failed to create the info bar"); + infoBar.ActionItemClicked += InfoBar_ActionItemClicked; - var textSpans = new[] - { - new InfoBarTextSpan(message), - new InfoBarHyperlink("Copy details", transitiveDependencies) - }; + _infoBars.Add(infoBar); - ShowInfoBar(textSpans).FireAndForget(); - } + await infoBar.TryShowInfoBarUIAsync(); - private static void ShowInfoBar(string text, TimeSpan? timeOut = default) - { - ShowInfoBar(new[] { new InfoBarTextSpan(text) }, timeOut).FireAndForget(); - } + if (timeOut.HasValue) + { + await Task.Delay(timeOut.Value); + infoBar.Close(); + _infoBars.Remove(infoBar); + } + } - private static async Task ShowInfoBar(IEnumerable textSpans, TimeSpan? timeOut = default) - { - var model = new InfoBarModel(textSpans, KnownMonikers.NuGet, isCloseButtonVisible: true); - - var infoBar = await VS.InfoBar.CreateAsync(ToolWindowGuids80.SolutionExplorer, model) ?? throw new InvalidOperationException("Failed to create the info bar"); - infoBar.ActionItemClicked += InfoBar_ActionItemClicked; - - _infoBars.Add(infoBar); - - await infoBar.TryShowInfoBarUIAsync(); - - if (timeOut.HasValue) - { - await Task.Delay(timeOut.Value); - infoBar.Close(); - _infoBars.Remove(infoBar); - } - } - - public static void CloseInfoBars() - { - _infoBars.ForEach(item => item.Close()); - _infoBars.Clear(); - } - - private static void InfoBar_ActionItemClicked(object sender, InfoBarActionItemEventArgs e) - { - ThrowIfNotOnUIThread(); - - switch (e.ActionItem.ActionContext) - { - case Actions.Manage: - NuGetMonitorCommands.Instance?.ShowMonitorToolWindow(); - break; - - case ICollection transitiveDependencies: - PrintDependencyTree(transitiveDependencies); - break; - } - - if (GeneralOptions.Instance.CloseInfoBar) - { - (sender as InfoBar)?.Close(); - } - } - - private static void PrintDependencyTree(IEnumerable dependencies) - { - var text = new StringBuilder(); - - foreach (var dependency in dependencies) - { - var (_, packages) = dependency; - - var projectName = dependency.ProjectName; - var targetFramework = dependency.TargetFramework; - - var vulnerablePackages = packages - .Select(item => item.Key) - .Where(item => item.IsVulnerable && item.VulnerabilityMitigation.IsNullOrEmpty()) - .ToArray(); - - if (vulnerablePackages.Length == 0) - continue; - - var header = $"{projectName}, {targetFramework}"; - - text.AppendLine(header) - .AppendLine(new string('-', header.Length)); - - foreach (var vulnerablePackage in vulnerablePackages) - { - PrintDependencyTree(text, vulnerablePackage, packages, 0); - } - - text.AppendLine().AppendLine(); - } - - Clipboard.SetText(text.ToString()); - - ShowInfoBar("Dependency tree copied to clipboard", TimeSpan.FromSeconds(10)); - } - - private static void PrintDependencyTree(StringBuilder text, PackageInfo package, IReadOnlyDictionary> parentsByChild, int nesting) - { - var indent = new string(' ', nesting * 4); - - text.Append(indent); - text.Append(package.PackageIdentity); - - if (package.IsDeprecated) - text.Append(" - Deprecated"); - - if (package.IsOutdated) - text.Append(" - Outdated"); - - if (package.Vulnerabilities?.Count > 0) - { - text.Append(" - Vulnerable:"); - foreach (var item in package.Vulnerabilities) - { - text.Append($" [ Severity: {item.Severity}, {item.AdvisoryUrl} ]"); - } - } - - text.AppendLine(); - - if (!parentsByChild.TryGetValue(package, out var dependsOn)) - return; - - foreach (var item in dependsOn) - { - PrintDependencyTree(text, item, parentsByChild, nesting + 1); - } - } - - private static IEnumerable GetInfoTexts(IEnumerable topLevelPackageInfos) - { - // Idea for showing counts, not sure if unicode icons in a InfoBar feel native - // new InfoBarTextSpan($"NuGet update: 🔼 {outdatedCount} ⚠ {deprecatedCount} 💀 {vulnerableCount}. "), - - var topLevelPackages = topLevelPackageInfos - .Where(item => item.PackageReferenceEntries.Any(entry => NuGetVersion.TryParse(entry.Identity.VersionRange.OriginalString, out _))) - .Select(item => item.PackageInfo) - .ToArray(); - - yield return topLevelPackages.CountedDescription("update", item => item.IsOutdated); - yield return topLevelPackages.CountedDescription("deprecation", item => item.IsDeprecated); - yield return topLevelPackages.CountedDescription("vulnerability", item => item.IsVulnerable && item.VulnerabilityMitigation.IsNullOrEmpty()); - } + public static void CloseInfoBars() + { + _infoBars.ForEach(item => item.Close()); + _infoBars.Clear(); + } + + private static void InfoBar_ActionItemClicked(object sender, InfoBarActionItemEventArgs e) + { + ThrowIfNotOnUIThread(); + + switch (e.ActionItem.ActionContext) + { + case Actions.Manage: + GeneralOptions.Instance.OpenNuGetPackageManager + ? VS.Commands.ExecuteAsync("Tools.ManageNuGetPackagesForSolution").FireAndForget() + : NuGetMonitorCommands.Instance?.ShowMonitorToolWindow(); + break; + + case ICollection transitiveDependencies: + PrintDependencyTree(transitiveDependencies); + break; + } + + if (GeneralOptions.Instance.CloseInfoBar) + { + (sender as InfoBar)?.Close(); + } + } + + private static void PrintDependencyTree(IEnumerable dependencies) + { + var text = new StringBuilder(); + + foreach (var dependency in dependencies) + { + var (_, packages) = dependency; + + var projectName = dependency.ProjectName; + var targetFramework = dependency.TargetFramework; + + var vulnerablePackages = packages + .Select(item => item.Key) + .Where(item => item.IsVulnerable && item.VulnerabilityMitigation.IsNullOrEmpty()) + .ToArray(); + + if (vulnerablePackages.Length == 0) + continue; + + var header = $"{projectName}, {targetFramework}"; + + text.AppendLine(header) + .AppendLine(new string('-', header.Length)); + + foreach (var vulnerablePackage in vulnerablePackages) + { + PrintDependencyTree(text, vulnerablePackage, packages, 0); + } + + text.AppendLine().AppendLine(); + } + + Clipboard.SetText(text.ToString()); + + ShowInfoBar("Dependency tree copied to clipboard", TimeSpan.FromSeconds(10)); + } + + private static void PrintDependencyTree(StringBuilder text, PackageInfo package, IReadOnlyDictionary> parentsByChild, int nesting) + { + var indent = new string(' ', nesting * 4); + + text.Append(indent); + text.Append(package.PackageIdentity); + + if (package.IsDeprecated) + text.Append(" - Deprecated"); + + if (package.IsOutdated) + text.Append(" - Outdated"); + + if (package.Vulnerabilities?.Count > 0) + { + text.Append(" - Vulnerable:"); + foreach (var item in package.Vulnerabilities) + { + text.Append($" [ Severity: {item.Severity}, {item.AdvisoryUrl} ]"); + } + } + + text.AppendLine(); + + if (!parentsByChild.TryGetValue(package, out var dependsOn)) + return; + + foreach (var item in dependsOn) + { + PrintDependencyTree(text, item, parentsByChild, nesting + 1); + } + } + + private static IEnumerable GetInfoTexts(IEnumerable topLevelPackageInfos) + { + // Idea for showing counts, not sure if unicode icons in a InfoBar feel native + // new InfoBarTextSpan($"NuGet update: 🔼 {outdatedCount} ⚠ {deprecatedCount} 💀 {vulnerableCount}. "), + + var topLevelPackages = topLevelPackageInfos + .Where(item => item.PackageReferenceEntries.Any(entry => NuGetVersion.TryParse(entry.Identity.VersionRange.OriginalString, out _))) + .Select(item => item.PackageInfo) + .ToArray(); + + yield return topLevelPackages.CountedDescription("update", item => item.IsOutdated); + yield return topLevelPackages.CountedDescription("deprecation", item => item.IsDeprecated); + yield return topLevelPackages.CountedDescription("vulnerability", item => item.IsVulnerable && item.VulnerabilityMitigation.IsNullOrEmpty()); + } } \ No newline at end of file