File: AddPackage\ParentInstallPackageCodeAction.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.AddImport;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Packaging;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.AddPackage;
 
/// <summary>
/// Data used to create the 'Install Nuget Package' top level code-action. It will have children to 'Install Latest',
/// 'Install Version 'X' ..., and 'Install with package manager'.
/// </summary>
/// <param name="packageSource">The nuget source to use.  Currently this is only <see
/// cref="PackageSourceHelper.NugetOrg"/> ("nuget.org").  Can be <see langword="null"/> to use the users configured
/// sources.</param>
/// <param name="packageName">The name of the package to install.</param>
/// <param name="packageVersionOpt">A optional preferred version if known. If not present, the user will be given the
/// option to either install the latest version, or install any version installed locally in another project.</param>
/// <param name="textChanges">Additional text changes to make to the <see cref="Document"/>.  Generally, this would be
/// the import to add if not present.</param>
internal readonly struct InstallPackageData(string? packageSource, string packageName, string? packageVersionOpt, ImmutableArray<TextChange> textChanges)
{
    public readonly string? PackageSource = packageSource;
    public readonly string PackageName = packageName;
    public readonly string? PackageVersionOpt = packageVersionOpt;
 
    public readonly ImmutableArray<TextChange> TextChanges = textChanges;
}
 
/// <summary>
/// This is the top level 'Install Nuget Package' code action we show in 
/// the lightbulb.  It will have children to 'Install Latest', 
/// 'Install Version 'X' ..., and 'Install with package manager'.
/// </summary>
internal sealed class ParentInstallPackageCodeAction : CodeAction.CodeActionWithNestedActions
{
    /// <summary>
    /// This code action only works by installing a package.  As such, it requires a non document change (and is
    /// thus restricted in which hosts it can run).
    /// </summary>
    public override ImmutableArray<string> Tags => RequiresNonDocumentChangeTags;
 
    /// <summary>
    /// Even though we have child actions, we mark ourselves as explicitly non-inlinable.
    /// We want to the experience of having the top level item the user has to see and
    /// navigate through, and we don't want our child items confusingly being added to the
    /// top level light-bulb where it's not clear what effect they would have if invoked.
    /// </summary>
    public ParentInstallPackageCodeAction(
        Document document,
        InstallPackageData fixData,
        IPackageInstallerService installerService)
        : base(string.Format(FeaturesResources.Install_package_0, fixData.PackageName),
               CreateNestedActions(document, fixData, installerService),
               isInlinable: false,
               priority: CodeActionPriority.Low) // Adding a nuget reference is lower priority than other fixes..
    {
    }
 
    public static CodeAction? TryCreateCodeAction(
        Document document,
        InstallPackageData fixData,
        IPackageInstallerService? installerService)
    {
        installerService ??= document.Project.Solution.Services.GetService<IPackageInstallerService>();
 
        return installerService?.IsInstalled(document.Project.Id, fixData.PackageName) == false
            ? new ParentInstallPackageCodeAction(document, fixData, installerService)
            : null;
    }
 
    private static ImmutableArray<CodeAction> CreateNestedActions(
        Document document,
        InstallPackageData fixData,
        IPackageInstallerService installerService)
    {
        // Determine what versions of this package are already installed in some project
        // in this solution.  We'll offer to add those specific versions to this project,
        // followed by an option to "Find and install latest version."
        var installedVersions = installerService.GetInstalledVersions(fixData.PackageName).NullToEmpty();
        using var _ = ArrayBuilder<CodeAction>.GetInstance(out var codeActions);
 
        // First add the actions to install a specific version.
        codeActions.AddRange(installedVersions.Select(
            v => CreateCodeAction(
                document, fixData, installerService, version: v, isLocal: true)));
 
        // Now add the action to install the specific version.
        var preferredVersion = fixData.PackageVersionOpt;
        if (preferredVersion == null || !installedVersions.Contains(preferredVersion))
        {
            codeActions.Add(CreateCodeAction(
                document, fixData, installerService, preferredVersion, isLocal: false));
        }
 
        // And finally the action to show the package manager dialog.
        codeActions.Add(new InstallWithPackageManagerCodeAction(installerService, fixData.PackageName));
        return codeActions.ToImmutableAndClear();
    }
 
    private static CodeAction CreateCodeAction(
        Document document,
        InstallPackageData installData,
        IPackageInstallerService installerService,
        string? version,
        bool isLocal)
    {
        var title = version == null
            ? FeaturesResources.Find_and_install_latest_version
            : isLocal
                ? string.Format(FeaturesResources.Use_local_version_0, version)
                : string.Format(FeaturesResources.Install_version_0, version);
 
        var installOperation = new InstallPackageDirectlyCodeActionOperation(
            installerService, document, installData.PackageSource, installData.PackageName, version,
            includePrerelease: false, isLocal: isLocal);
 
        // Nuget hits should always come after other results.
        var fixData = new AddImportFixData(
            AddImportFixKind.PackageSymbol, installData.TextChanges, title, priority: CodeActionPriority.Lowest,
            packageSource: installData.PackageSource, packageName: installData.PackageName, packageVersionOpt: installData.PackageVersionOpt);
        return new InstallPackageAndAddImportCodeAction(
            document, fixData, title, installOperation);
    }
}