|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Packaging;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.SymbolSearch;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.LanguageServices.SymbolSearch;
using Microsoft.VisualStudio.LanguageServices.Utilities;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell.ServiceBroker;
using Microsoft.VisualStudio.Threading;
using NuGet.VisualStudio;
using NuGet.VisualStudio.Contracts;
using Roslyn.Utilities;
using SVsServiceProvider = Microsoft.VisualStudio.Shell.SVsServiceProvider;
using VSUtilities = Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Packaging;
/// <summary>
/// Free threaded wrapper around the NuGet.VisualStudio STA package installer interfaces.
/// We want to be able to make queries about packages from any thread. For example, the
/// add-NuGet-reference feature wants to know what packages a project already has
/// references to. NuGet.VisualStudio provides this information, but only in a COM STA
/// manner. As we don't want our background work to bounce and block on the UI thread
/// we have this helper class which queries the information on the UI thread and caches
/// the data so it can be read from the background.
/// </summary>
[ExportWorkspaceService(typeof(IPackageInstallerService)), Shared]
internal partial class PackageInstallerService : AbstractDelayStartedService, IPackageInstallerService, IVsSearchProviderCallback
{
// Proper name, should not be localized.
private const string NugetTitle = "NuGet";
private readonly VSUtilities.IUIThreadOperationExecutor _operationExecutor;
private readonly IVsService<IBrokeredServiceContainer> _brokeredServiceContainer;
private readonly SVsServiceProvider _serviceProvider;
private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactoryService;
private readonly IAsynchronousOperationListener _listener;
private readonly Lazy<IVsPackageInstaller2>? _packageInstaller;
private readonly Lazy<IVsPackageUninstaller>? _packageUninstaller;
private readonly Lazy<IVsPackageSourceProvider>? _packageSourceProvider;
private IVsPackage? _nugetPackageManager;
/// <summary>
/// Used to keep track of what types of changes we've seen so we can then determine what to refresh on the UI
/// thread. If we hear about project changes, we only refresh that project. If we hear about a solution level
/// change, we'll refresh all projects.
/// </summary>
/// <remarks>
/// <c>solutionChanged == true iff changedProject == null</c> and <c>solutionChanged == false iff changedProject
/// != null</c>. So technically having both values is redundant. However, i like the clarity of having both.
/// </remarks>
private readonly AsyncBatchingWorkQueue<(bool solutionChanged, ProjectId? changedProject)> _workQueue;
private readonly ConcurrentDictionary<ProjectId, ProjectState> _projectToInstalledPackageAndVersion = [];
/// <summary>
/// Lock used to protect reads and writes of <see cref="_packageSourcesTask"/>.
/// </summary>
private readonly object _gate = new();
/// <summary>
/// Task uses to compute the set of package sources on demand when asked the first time. The value will be
/// computed and cached in the task. When this value changes, the task will simply be cleared out.
/// </summary>
private Task<ImmutableArray<PackageSource>>? _packageSourcesTask;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public PackageInstallerService(
IThreadingContext threadingContext,
VSUtilities.IUIThreadOperationExecutor operationExecutor,
IAsynchronousOperationListenerProvider listenerProvider,
VisualStudioWorkspaceImpl workspace,
IGlobalOptionService globalOptions,
IVsService<SVsBrokeredServiceContainer, IBrokeredServiceContainer> brokeredServiceContainer,
SVsServiceProvider serviceProvider,
IVsEditorAdaptersFactoryService editorAdaptersFactoryService,
[Import(AllowDefault = true)] Lazy<IVsPackageInstaller2>? packageInstaller,
[Import(AllowDefault = true)] Lazy<IVsPackageUninstaller>? packageUninstaller,
[Import(AllowDefault = true)] Lazy<IVsPackageSourceProvider>? packageSourceProvider)
: base(threadingContext,
globalOptions,
workspace,
listenerProvider,
SymbolSearchGlobalOptionsStorage.Enabled,
[SymbolSearchOptionsStorage.SearchReferenceAssemblies, SymbolSearchOptionsStorage.SearchNuGetPackages])
{
_operationExecutor = operationExecutor;
_brokeredServiceContainer = brokeredServiceContainer;
_serviceProvider = serviceProvider;
_editorAdaptersFactoryService = editorAdaptersFactoryService;
_packageInstaller = packageInstaller;
_packageUninstaller = packageUninstaller;
_packageSourceProvider = packageSourceProvider;
_listener = listenerProvider.GetListener(FeatureAttribute.PackageInstaller);
// Setup the work queue to allow us to hear about flurries of changes and then respond to them in batches
// every second. Note: we pass in EqualityComparer<...>.Default since we don't care about ordering, and
// since once we hear about changes to a project (or the whole solution), we don't need to keep track if we
// hear about the same thing in that batch window interval.
_workQueue = new AsyncBatchingWorkQueue<(bool solutionChanged, ProjectId? changedProject)>(
TimeSpan.FromSeconds(1),
this.ProcessWorkQueueAsync,
equalityComparer: EqualityComparer<(bool solutionChanged, ProjectId? changedProject)>.Default,
_listener,
this.DisposalToken);
}
public event EventHandler? PackageSourcesChanged;
public ImmutableArray<PackageSource> TryGetPackageSources()
{
Task<ImmutableArray<PackageSource>> localPackageSourcesTask;
lock (_gate)
{
_packageSourcesTask ??= Task.Run(() => GetPackageSourcesAsync(), this.DisposalToken);
localPackageSourcesTask = _packageSourcesTask;
}
if (localPackageSourcesTask.Status == TaskStatus.RanToCompletion)
{
return localPackageSourcesTask.Result;
}
else
{
// The result was not available yet (or it was canceled/faulted). Just return an empty result to
// signify we couldn't get this right now.
return ImmutableArray<PackageSource>.Empty;
}
}
private async Task<ImmutableArray<PackageSource>> GetPackageSourcesAsync()
{
await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(DisposalToken);
try
{
if (_packageSourceProvider != null)
return _packageSourceProvider.Value.GetSources(includeUnOfficial: true, includeDisabled: false).SelectAsArray(r => new PackageSource(r.Key, r.Value));
}
catch (Exception ex) when (ex is InvalidDataException or InvalidOperationException)
{
// These exceptions can happen when the nuget.config file is broken.
}
catch (ArgumentException ae) when (FatalError.ReportAndCatch(ae))
{
// This exception can happen when the nuget.config file is broken, e.g. invalid credentials.
// https://github.com/dotnet/roslyn/issues/40857
}
return ImmutableArray<PackageSource>.Empty;
}
[MemberNotNullWhen(true, nameof(_packageInstaller))]
[MemberNotNullWhen(true, nameof(_packageUninstaller))]
[MemberNotNullWhen(true, nameof(_packageSourceProvider))]
private bool IsEnabled
{
get
{
return _packageInstaller != null
&& _packageUninstaller != null
&& _packageSourceProvider != null;
}
}
bool IPackageInstallerService.IsEnabled(ProjectId projectId)
{
if (!IsEnabled)
{
return false;
}
if (_projectToInstalledPackageAndVersion.TryGetValue(projectId, out var state))
{
return state.IsEnabled;
}
// If we haven't scanned the project yet, assume that we're available for it.
return true;
}
private async ValueTask<IVsPackageSourceProvider> GetPackageSourceProviderAsync()
{
Contract.ThrowIfFalse(IsEnabled);
if (!_packageSourceProvider.IsValueCreated)
{
// Switch to background thread for known assembly load
await TaskScheduler.Default;
}
return _packageSourceProvider.Value;
}
protected override async Task EnableServiceAsync(CancellationToken cancellationToken)
{
if (!IsEnabled)
return;
var packageSourceProvider = await GetPackageSourceProviderAsync().ConfigureAwait(false);
// Start listening to additional events workspace changes.
Workspace.WorkspaceChanged += OnWorkspaceChanged;
packageSourceProvider.SourcesChanged += OnSourceProviderSourcesChanged;
// Kick off an initial set of work that will analyze the entire solution.
OnSourceProviderSourcesChanged(this, EventArgs.Empty);
_workQueue.AddWork((solutionChanged: true, changedProject: null));
}
private void OnSourceProviderSourcesChanged(object sender, EventArgs e)
{
lock (_gate)
{
// If the existing _packageSourcesTask is null, that means no one has asked us about package sources
// yet. So no need for us to do anything if that's true. We'll just continue waiting until first
// asked. However, if it's not null, that means we have already been asked. In that case, proactively
// get the new set of sources so they're ready for the next time we're asked.
if (_packageSourcesTask != null)
_packageSourcesTask = Task.Run(() => GetPackageSourcesAsync(), this.DisposalToken);
}
PackageSourcesChanged?.Invoke(this, EventArgs.Empty);
}
public async Task<bool> TryInstallPackageAsync(
Workspace workspace,
DocumentId documentId,
string? source,
string packageName,
string? version,
bool includePrerelease,
IProgress<CodeAnalysisProgress> progressTracker,
CancellationToken cancellationToken)
{
// The 'workspace == _workspace' line is probably not necessary. However, we include
// it just to make sure that someone isn't trying to install a package into a workspace
// other than the VisualStudioWorkspace.
if (workspace == Workspace && Workspace != null && IsEnabled)
{
await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var projectId = documentId.ProjectId;
var dte = (EnvDTE.DTE)_serviceProvider.GetService(typeof(SDTE));
var dteProject = Workspace.TryGetDTEProject(projectId);
var projectGuid = Workspace.GetProjectGuid(projectId);
if (dteProject != null && projectGuid != Guid.Empty)
{
var undoManager = _editorAdaptersFactoryService.TryGetUndoManager(
workspace, documentId, cancellationToken);
return await TryInstallAndAddUndoActionAsync(
source, packageName, version, includePrerelease, projectGuid, dte, dteProject, undoManager,
progressTracker, cancellationToken).ConfigureAwait(false);
}
}
return false;
}
private async Task<bool> TryInstallPackageAsync(
string? source,
string packageName,
string? version,
bool includePrerelease,
Guid projectGuid,
EnvDTE.DTE dte,
EnvDTE.Project dteProject,
IProgress<CodeAnalysisProgress> progressTracker,
CancellationToken cancellationToken)
{
Contract.ThrowIfFalse(IsEnabled);
var description = string.Format(ServicesVSResources.Installing_0, packageName);
progressTracker.Report(CodeAnalysisProgress.Description(description));
await UpdateStatusBarAsync(dte, description, cancellationToken).ConfigureAwait(false);
try
{
return await this.PerformNuGetProjectServiceWorkAsync(async (nugetService, cancellationToken) =>
{
// explicitly switch to BG thread to do the installation as nuget installer APIs are free threaded.
await TaskScheduler.Default;
var installedPackagesMap = await GetInstalledPackagesMapAsync(nugetService, projectGuid, cancellationToken).ConfigureAwait(false);
if (installedPackagesMap.ContainsKey(packageName))
return false;
// Once we start the installation, we can't cancel anymore.
cancellationToken = default;
if (version == null)
{
_packageInstaller.Value.InstallLatestPackage(
source, dteProject, packageName, includePrerelease, ignoreDependencies: false);
}
else
{
_packageInstaller.Value.InstallPackage(
source, dteProject, packageName, version, ignoreDependencies: false);
}
installedPackagesMap = await GetInstalledPackagesMapAsync(nugetService, projectGuid, cancellationToken).ConfigureAwait(false);
var installedVersion = installedPackagesMap.TryGetValue(packageName, out var result) ? result : null;
await UpdateStatusBarAsync(
dte, string.Format(ServicesVSResources.Installing_0_completed, GetStatusBarText(packageName, installedVersion)),
cancellationToken).ConfigureAwait(false);
return true;
}, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
await UpdateStatusBarAsync(dte, ServicesVSResources.Package_install_canceled, cancellationToken).ConfigureAwait(false);
return false;
}
catch (Exception e) when (FatalError.ReportAndCatch(e))
{
await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
dte.StatusBar.Text = string.Format(ServicesVSResources.Package_install_failed_colon_0, e.Message);
var notificationService = Workspace.Services.GetService<INotificationService>();
notificationService?.SendNotification(
string.Format(ServicesVSResources.Installing_0_failed_Additional_information_colon_1, packageName, e.Message),
severity: NotificationSeverity.Error);
return false;
}
}
private async Task UpdateStatusBarAsync(EnvDTE.DTE dte, string text, CancellationToken cancellationToken)
{
await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
dte.StatusBar.Text = text;
}
private static string GetStatusBarText(string packageName, string? installedVersion)
=> installedVersion == null ? packageName : $"{packageName} - {installedVersion}";
private async Task<bool> TryUninstallPackageAsync(
string packageName, Guid projectGuid, EnvDTE.DTE dte, EnvDTE.Project dteProject,
IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
{
Contract.ThrowIfFalse(IsEnabled);
var description = string.Format(ServicesVSResources.Uninstalling_0, packageName);
progressTracker.Report(CodeAnalysisProgress.Description(description));
await UpdateStatusBarAsync(dte, description, cancellationToken).ConfigureAwait(false);
try
{
return await this.PerformNuGetProjectServiceWorkAsync(async (nugetService, cancellationToken) =>
{
// explicitly switch to BG thread to do the installation as nuget installer APIs are free threaded.
await TaskScheduler.Default;
var installedPackagesMap = await GetInstalledPackagesMapAsync(nugetService, projectGuid, cancellationToken).ConfigureAwait(false);
if (!installedPackagesMap.TryGetValue(packageName, out var installedVersion))
return false;
cancellationToken.ThrowIfCancellationRequested();
// Once we start the installation, we can't cancel anymore.
cancellationToken = default;
_packageUninstaller.Value.UninstallPackage(dteProject, packageName, removeDependencies: true);
await UpdateStatusBarAsync(
dte, string.Format(ServicesVSResources.Uninstalling_0_completed, GetStatusBarText(packageName, installedVersion)), cancellationToken).ConfigureAwait(false);
return true;
}, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
await UpdateStatusBarAsync(dte, ServicesVSResources.Package_uninstall_canceled, cancellationToken).ConfigureAwait(false);
return false;
}
catch (Exception e) when (FatalError.ReportAndCatch(e))
{
await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
dte.StatusBar.Text = string.Format(ServicesVSResources.Package_uninstall_failed_colon_0, e.Message);
var notificationService = Workspace.Services.GetService<INotificationService>();
notificationService?.SendNotification(
string.Format(ServicesVSResources.Uninstalling_0_failed_Additional_information_colon_1, packageName, e.Message),
severity: NotificationSeverity.Error);
return false;
}
}
private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
{
// ThisCanBeCalledOnAnyThread();
var solutionChanged = false;
ProjectId? changedProject = null;
switch (e.Kind)
{
default:
// Nothing to do for any other events.
return;
case WorkspaceChangeKind.ProjectAdded:
case WorkspaceChangeKind.ProjectChanged:
case WorkspaceChangeKind.ProjectReloaded:
case WorkspaceChangeKind.ProjectRemoved:
changedProject = e.ProjectId;
break;
case WorkspaceChangeKind.SolutionAdded:
case WorkspaceChangeKind.SolutionChanged:
case WorkspaceChangeKind.SolutionCleared:
case WorkspaceChangeKind.SolutionReloaded:
case WorkspaceChangeKind.SolutionRemoved:
solutionChanged = true;
break;
}
Contract.ThrowIfNull(_workQueue, "We should only register for events after having create the WorkQueue");
_workQueue.AddWork((solutionChanged, changedProject));
}
private ValueTask ProcessWorkQueueAsync(
ImmutableSegmentedList<(bool solutionChanged, ProjectId? changedProject)> workQueue, CancellationToken cancellationToken)
{
// ThisCanBeCalledOnAnyThread();
Contract.ThrowIfNull(_workQueue, "How could we be processing a workqueue change without a workqueue?");
// If we've been disconnected, then there's no point proceeding.
if (Workspace == null || !IsEnabled)
return ValueTaskFactory.CompletedTask;
return ProcessWorkQueueWorkerAsync(workQueue, cancellationToken);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private async ValueTask ProcessWorkQueueWorkerAsync(
ImmutableSegmentedList<(bool solutionChanged, ProjectId? changedProject)> workQueue, CancellationToken cancellationToken)
{
// ThisCanBeCalledOnAnyThread();
await PerformNuGetProjectServiceWorkAsync<object>(async (nugetService, cancellationToken) =>
{
// Figure out the entire set of projects to process.
using var _ = PooledHashSet<ProjectId>.GetInstance(out var projectsToProcess);
var solution = Workspace.CurrentSolution;
AddProjectsToProcess(workQueue, solution, projectsToProcess);
// And Process them one at a time.
foreach (var projectId in projectsToProcess)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessProjectChangeAsync(nugetService, solution, projectId, cancellationToken).ConfigureAwait(false);
}
return null;
}, cancellationToken).ConfigureAwait(false);
}
private async Task<T?> PerformNuGetProjectServiceWorkAsync<T>(
Func<INuGetProjectService, CancellationToken, ValueTask<T?>> doWorkAsync, CancellationToken cancellationToken)
{
// Make sure we are on the thread pool to avoid UI thread dependencies if external code uses ConfigureAwait(true).
// GetServiceAsync/GetProxyAsync and the cast below are all explicitly documented as being BG thread safe.
await TaskScheduler.Default;
var serviceContainer = await _brokeredServiceContainer.GetValueOrNullAsync(cancellationToken).ConfigureAwait(false);
var serviceBroker = serviceContainer?.GetFullAccessServiceBroker();
if (serviceBroker == null)
return default;
var nugetService = await serviceBroker.GetProxyAsync<INuGetProjectService>(NuGetServices.NuGetProjectServiceV1, cancellationToken: cancellationToken).ConfigureAwait(false);
using (nugetService as IDisposable)
{
// If we didn't get a nuget service, there's nothing we can do in terms of querying the solution for
// nuget info.
if (nugetService == null)
return default;
return await doWorkAsync(nugetService, cancellationToken).ConfigureAwait(false);
}
}
private void AddProjectsToProcess(
ImmutableSegmentedList<(bool solutionChanged, ProjectId? changedProject)> workQueue, Solution solution, HashSet<ProjectId> projectsToProcess)
{
// ThisCanBeCalledOnAnyThread();
// If we detected a solution change, then we need to process all projects.
// This includes all the projects that we already know about, as well as
// all the projects in the current workspace solution.
if (workQueue.Any(t => t.solutionChanged))
{
projectsToProcess.AddRange(solution.ProjectIds);
projectsToProcess.AddRange(_projectToInstalledPackageAndVersion.Keys);
}
else
{
projectsToProcess.AddRange(workQueue.Select(t => t.changedProject).WhereNotNull());
}
}
private async Task ProcessProjectChangeAsync(
INuGetProjectService nugetService,
Solution solution,
ProjectId projectId,
CancellationToken cancellationToken)
{
// ThisCanBeCalledOnAnyThread();
var project = solution.GetProject(projectId);
// We really only need to know the NuGet status for managed language projects.
// Also, the NuGet APIs may throw on some projects that don't implement the
// full set of DTE APIs they expect. So we filter down to just C# and VB here
// as we know these languages are safe to build up this index for.
ProjectState? newState = null;
if (project?.Language is LanguageNames.CSharp or
LanguageNames.VisualBasic)
{
var projectGuid = Workspace.GetProjectGuid(projectId);
if (projectGuid != Guid.Empty)
{
newState = await GetCurrentProjectStateAsync(
nugetService, projectGuid, cancellationToken).ConfigureAwait(false);
}
}
// If we weren't able to get the nuget state for the project (i.e. it's not a c#/vb project, or we got a
// crash attempting to get nuget information). Mark this project as something that nuget-add-import is not
// supported for.
_projectToInstalledPackageAndVersion[projectId] = newState ?? ProjectState.Disabled;
}
private static async Task<ProjectState?> GetCurrentProjectStateAsync(
INuGetProjectService nugetService,
Guid projectGuid,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var installedPackagesMap = await GetInstalledPackagesMapAsync(nugetService, projectGuid, cancellationToken).ConfigureAwait(false);
return new ProjectState(installedPackagesMap);
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
{
return null;
}
}
private static async Task<ImmutableDictionary<string, string>> GetInstalledPackagesMapAsync(INuGetProjectService nugetService, Guid projectGuid, CancellationToken cancellationToken)
{
var installedPackagesResult = await nugetService.GetInstalledPackagesAsync(projectGuid, cancellationToken).ConfigureAwait(false);
using var _ = PooledDictionary<string, string>.GetInstance(out var installedPackages);
if (installedPackagesResult?.Status == InstalledPackageResultStatus.Successful)
{
foreach (var installedPackage in installedPackagesResult.Packages)
installedPackages[installedPackage.Id] = installedPackage.Version;
}
var installedPackagesMap = installedPackages.ToImmutableDictionary();
return installedPackagesMap;
}
public bool IsInstalled(ProjectId projectId, string packageName)
{
// ThisCanBeCalledOnAnyThread();
return _projectToInstalledPackageAndVersion.TryGetValue(projectId, out var installedPackages) &&
installedPackages.IsInstalled(packageName);
}
public ImmutableArray<string> GetInstalledVersions(string packageName)
{
// ThisCanBeCalledOnAnyThread();
using var _ = PooledHashSet<string>.GetInstance(out var installedVersions);
foreach (var state in _projectToInstalledPackageAndVersion.Values)
{
if (state.TryGetInstalledVersion(packageName, out var version))
installedVersions.Add(version);
}
// Order the versions with a weak heuristic so that 'newer' versions come first.
// Essentially, we try to break the version on dots, and then we use a LogicalComparer
// to try to more naturally order the things we see between the dots.
var versionsAndSplits = installedVersions.Select(v => new { Version = v, Split = v.Split('.') }).ToList();
versionsAndSplits.Sort((v1, v2) =>
{
var diff = CompareSplit(v1.Split, v2.Split);
return diff != 0 ? diff : -v1.Version.CompareTo(v2.Version);
});
return versionsAndSplits.Select(v => v.Version).ToImmutableArray();
}
private static int CompareSplit(string[] split1, string[] split2)
{
// ThisCanBeCalledOnAnyThread();
for (int i = 0, n = Math.Min(split1.Length, split2.Length); i < n; i++)
{
// Prefer things that look larger. i.e. 7 should come before 6.
// Use a logical string comparer so that 10 is understood to be
// greater than 3.
var diff = -LogicalStringComparer.Instance.Compare(split1[i], split2[i]);
if (diff != 0)
{
return diff;
}
}
// Choose the one with more parts.
return split2.Length - split1.Length;
}
public ImmutableArray<Project> GetProjectsWithInstalledPackage(Solution solution, string packageName, string version)
{
// ThisCanBeCalledOnAnyThread();
using var _ = ArrayBuilder<Project>.GetInstance(out var result);
foreach (var (projectId, state) in _projectToInstalledPackageAndVersion)
{
if (state.TryGetInstalledVersion(packageName, out var installedVersion) &&
installedVersion == version)
{
var project = solution.GetProject(projectId);
if (project != null)
result.Add(project);
}
}
return result.ToImmutableAndClear();
}
public bool CanShowManagePackagesDialog()
=> TryGetOrLoadNuGetPackageManager(out _);
private bool TryGetOrLoadNuGetPackageManager([NotNullWhen(true)] out IVsPackage? nugetPackageManager)
{
this.ThreadingContext.ThrowIfNotOnUIThread();
if (_nugetPackageManager != null)
{
nugetPackageManager = _nugetPackageManager;
return true;
}
nugetPackageManager = null;
var shell = (IVsShell)_serviceProvider.GetService(typeof(SVsShell));
if (shell == null)
{
return false;
}
var nugetGuid = new Guid("5fcc8577-4feb-4d04-ad72-d6c629b083cc");
shell.LoadPackage(ref nugetGuid, out nugetPackageManager);
_nugetPackageManager = nugetPackageManager;
return nugetPackageManager != null;
}
public void ShowManagePackagesDialog(string packageName)
{
if (!TryGetOrLoadNuGetPackageManager(out var nugetPackageManager))
{
return;
}
// We're able to launch the package manager (with an item in its search box) by
// using the IVsSearchProvider API that the NuGet package exposes.
//
// We get that interface for it and then pass it a SearchQuery that effectively
// wraps the package name we're looking for. The NuGet package will then read
// out that string and populate their search box with it.
var extensionProvider = (IVsPackageExtensionProvider)nugetPackageManager;
var extensionGuid = new Guid("042C2B4B-C7F7-49DB-B7A2-402EB8DC7892");
var emptyGuid = Guid.Empty;
var searchProvider = (IVsSearchProvider)extensionProvider.CreateExtensionInstance(ref emptyGuid, ref extensionGuid);
var task = searchProvider.CreateSearch(dwCookie: 1, pSearchQuery: new SearchQuery(packageName), pSearchCallback: this);
task.Start();
}
public void ReportProgress(IVsSearchTask pTask, uint dwProgress, uint dwMaxProgress)
{
}
public void ReportComplete(IVsSearchTask pTask, uint dwResultsFound)
{
}
public void ReportResult(IVsSearchTask pTask, IVsSearchItemResult pSearchItemResult)
=> pSearchItemResult.InvokeAction();
public void ReportResults(IVsSearchTask pTask, uint dwResults, IVsSearchItemResult[] pSearchItemResults)
{
}
private class SearchQuery : IVsSearchQuery
{
public SearchQuery(string packageName)
=> this.SearchString = packageName;
public string SearchString { get; }
public uint ParseError => 0;
public uint GetTokens(uint dwMaxTokens, IVsSearchToken[] rgpSearchTokens)
=> 0;
}
}
|