File: UnusedReferences\RemoveUnusedReferencesCommandHandler.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Immutable;
using System.ComponentModel.Design;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Options;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.UnusedReferences;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.LanguageServices.Implementation.UnusedReferences.Dialog;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Microsoft.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.UnusedReferences;
 
[Export(typeof(RemoveUnusedReferencesCommandHandler)), Shared]
internal sealed class RemoveUnusedReferencesCommandHandler
{
    private const string ProjectAssetsFilePropertyName = "ProjectAssetsFile";
    private readonly IThreadingContext _threadingContext;
    private readonly RemoveUnusedReferencesDialogProvider _unusedReferenceDialogProvider;
    private readonly VisualStudioWorkspace _workspace;
    private readonly IGlobalOptionService _globalOptions;
    private readonly IUIThreadOperationExecutor _threadOperationExecutor;
    private IServiceProvider? _serviceProvider;
 
    private IReferenceCleanupService ReferenceCleanupService
        => _workspace.Services.GetRequiredService<IReferenceCleanupService>();
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public RemoveUnusedReferencesCommandHandler(
        IThreadingContext threadingContext,
        RemoveUnusedReferencesDialogProvider unusedReferenceDialogProvider,
        IUIThreadOperationExecutor threadOperationExecutor,
        VisualStudioWorkspace workspace,
        IGlobalOptionService globalOptions)
    {
        _threadingContext = threadingContext;
        _unusedReferenceDialogProvider = unusedReferenceDialogProvider;
        _threadOperationExecutor = threadOperationExecutor;
        _workspace = workspace;
        _globalOptions = globalOptions;
    }
 
    public async Task InitializeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken)
    {
        Contract.ThrowIfNull(serviceProvider);
 
        _serviceProvider = (IServiceProvider)serviceProvider;
 
        // Hook up the "Remove Unused References" menu command for CPS based managed projects.
        var menuCommandService = await serviceProvider.GetServiceAsync<IMenuCommandService, IMenuCommandService>(_threadingContext.JoinableTaskFactory, throwOnFailure: false).ConfigureAwait(false);
        if (menuCommandService != null)
        {
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
            VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, ID.RoslynCommands.RemoveUnusedReferences, Guids.RoslynGroupId, OnRemoveUnusedReferencesForSelectedProject, OnRemoveUnusedReferencesForSelectedProjectStatus);
        }
    }
 
    private void OnRemoveUnusedReferencesForSelectedProjectStatus(object sender, EventArgs e)
    {
        var command = (OleMenuCommand)sender;
 
        // If the value is null it means user loads the value from previous build (at that moment it is in experiment)
        // Since the feature is on by default now, just set it to true
        var isOptionEnabled = _globalOptions.GetOption(FeatureOnOffOptions.OfferRemoveUnusedReferences) ?? true;
 
        var isDotNetCpsProject = VisualStudioCommandHandlerHelpers.TryGetSelectedProjectHierarchy(_serviceProvider, out var hierarchy) &&
            hierarchy.IsCapabilityMatch("CPS") &&
            hierarchy.IsCapabilityMatch(".NET");
 
        // Only show the "Remove Unused Reference" menu commands for CPS based managed projects.
        var visible = isOptionEnabled && isDotNetCpsProject;
        var enabled = false;
 
        if (visible)
        {
            enabled = !VisualStudioCommandHandlerHelpers.IsBuildActive();
        }
 
        if (command.Visible != visible)
        {
            command.Visible = visible;
        }
 
        if (command.Enabled != enabled)
        {
            command.Enabled = enabled;
        }
    }
 
    private void OnRemoveUnusedReferencesForSelectedProject(object sender, EventArgs args)
    {
        if (VisualStudioCommandHandlerHelpers.TryGetSelectedProjectHierarchy(_serviceProvider, out var hierarchy))
        {
            Solution? solution = null;
            string? projectFilePath = null;
            ImmutableArray<ReferenceUpdate> referenceUpdates = default;
            var status = _threadOperationExecutor.Execute(ServicesVSResources.Remove_Unused_References, ServicesVSResources.Analyzing_project_references, allowCancellation: true, showProgress: true, (operationContext) =>
            {
                (solution, projectFilePath, referenceUpdates) = GetUnusedReferencesForProjectHierarchy(hierarchy, operationContext.UserCancellationToken);
            });
 
            if (status == UIThreadOperationStatus.Canceled)
            {
                return;
            }
 
            if (solution is null ||
                projectFilePath is not string { Length: > 0 } ||
                referenceUpdates.IsEmpty)
            {
                MessageDialog.Show(ServicesVSResources.Remove_Unused_References, ServicesVSResources.No_unused_references_were_found, MessageDialogCommandSet.Ok);
                return;
            }
 
            var dialog = _unusedReferenceDialogProvider.CreateDialog();
            if (dialog.ShowModal(_threadingContext.JoinableTaskFactory, solution, projectFilePath, referenceUpdates) == false)
            {
                return;
            }
 
            // If we are removing, then that is a change or if we are newly marking a reference as TreatAsUsed,
            // then that is a change.
            var referenceChanges = referenceUpdates
                .Where(update => update.Action != UpdateAction.TreatAsUsed || !update.ReferenceInfo.TreatAsUsed)
                .ToImmutableArray();
 
            // If there are no changes, then we can return
            if (referenceChanges.IsEmpty)
            {
                return;
            }
 
            // Since undo/redo is not supported, get confirmation that we should apply these changes.
            var result = MessageDialog.Show(ServicesVSResources.Remove_Unused_References, ServicesVSResources.This_action_cannot_be_undone_Do_you_wish_to_continue, MessageDialogCommandSet.YesNo);
            if (result == MessageDialogCommand.No)
            {
                return;
            }
 
            _threadOperationExecutor.Execute(ServicesVSResources.Remove_Unused_References, ServicesVSResources.Updating_project_references, allowCancellation: false, showProgress: true, (operationContext) =>
            {
                ApplyUnusedReferenceUpdates(_threadingContext.JoinableTaskFactory, solution, projectFilePath, referenceChanges, CancellationToken.None);
            });
        }
 
        return;
    }
 
    private (Solution?, string?, ImmutableArray<ReferenceUpdate>) GetUnusedReferencesForProjectHierarchy(
        IVsHierarchy projectHierarchy,
        CancellationToken cancellationToken)
    {
        if (!TryGetPropertyValue(projectHierarchy, ProjectAssetsFilePropertyName, out var projectAssetsFile))
        {
            return (null, null, ImmutableArray<ReferenceUpdate>.Empty);
        }
 
        var projectFilePath = projectHierarchy.TryGetProjectFilePath();
        if (string.IsNullOrEmpty(projectFilePath))
        {
            return (null, null, ImmutableArray<ReferenceUpdate>.Empty);
        }
 
        var solution = _workspace.CurrentSolution;
 
        var unusedReferences = GetUnusedReferencesForProject(solution, projectFilePath!, projectAssetsFile, cancellationToken);
 
        return (solution, projectFilePath, unusedReferences);
    }
 
    private ImmutableArray<ReferenceUpdate> GetUnusedReferencesForProject(Solution solution, string projectFilePath, string projectAssetsFile, CancellationToken cancellationToken)
    {
        var unusedReferences = _threadingContext.JoinableTaskFactory.Run(async () =>
        {
            var projectReferences = await this.ReferenceCleanupService.GetProjectReferencesAsync(projectFilePath, cancellationToken).ConfigureAwait(true);
            var unusedReferenceAnalysisService = solution.Services.GetRequiredService<IUnusedReferenceAnalysisService>();
            return await unusedReferenceAnalysisService.GetUnusedReferencesAsync(solution, projectFilePath, projectAssetsFile, projectReferences, cancellationToken).ConfigureAwait(true);
        });
 
        var referenceUpdates = unusedReferences
            .Select(reference => new ReferenceUpdate(reference.TreatAsUsed ? UpdateAction.TreatAsUsed : UpdateAction.Remove, reference))
            .ToImmutableArray();
 
        return referenceUpdates;
    }
 
    private static void ApplyUnusedReferenceUpdates(JoinableTaskFactory joinableTaskFactory, Solution solution, string projectFilePath, ImmutableArray<ReferenceUpdate> referenceUpdates, CancellationToken cancellationToken)
    {
        joinableTaskFactory.Run(
            () => UnusedReferencesRemover.UpdateReferencesAsync(solution, projectFilePath, referenceUpdates, cancellationToken));
    }
 
    private static bool TryGetPropertyValue(IVsHierarchy hierarchy, string propertyName, [NotNullWhen(returnValue: true)] out string? propertyValue)
    {
        if (hierarchy is not IVsBuildPropertyStorage storage)
        {
            propertyValue = null;
            return false;
        }
 
        return ErrorHandler.Succeeded(storage.GetPropertyValue(propertyName, null, (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue));
    }
}