File: Suggestions\SuggestedActions\SuggestedAction.cs
Web Access
Project: src\src\EditorFeatures\Core.Wpf\Microsoft.CodeAnalysis.EditorFeatures.Wpf_per0z0nh_wpftmp.csproj (Microsoft.CodeAnalysis.EditorFeatures.Wpf)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeFixesAndRefactorings;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Progress;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Core.Imaging;
using Microsoft.VisualStudio.Imaging.Interop;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.Suggestions;
 
/// <summary>
/// Base class for all Roslyn light bulb menu items.
/// </summary>
internal abstract partial class SuggestedAction : ISuggestedAction3, IEquatable<ISuggestedAction>
{
    protected readonly IThreadingContext ThreadingContext;
    protected readonly SuggestedActionsSourceProvider SourceProvider;
 
    protected readonly Workspace Workspace;
    protected readonly Solution OriginalSolution;
    protected readonly ITextBuffer SubjectBuffer;
 
    protected readonly object Provider;
    internal readonly CodeAction CodeAction;
 
    private ICodeActionEditHandlerService EditHandler => SourceProvider.EditHandler;
 
    internal SuggestedAction(
        IThreadingContext threadingContext,
        SuggestedActionsSourceProvider sourceProvider,
        Workspace workspace,
        Solution originalSolution,
        ITextBuffer subjectBuffer,
        object provider,
        CodeAction codeAction)
    {
        Contract.ThrowIfNull(provider);
        Contract.ThrowIfNull(codeAction);
 
        ThreadingContext = threadingContext;
        SourceProvider = sourceProvider;
        Workspace = workspace;
        OriginalSolution = originalSolution;
        SubjectBuffer = subjectBuffer;
        Provider = provider;
        CodeAction = codeAction;
    }
 
    public virtual bool TryGetTelemetryId(out Guid telemetryId)
    {
        telemetryId = CodeAction.GetTelemetryId();
        return true;
    }
 
    protected async Task<ImmutableArray<CodeActionOperation>> GetOperationsAsync(
        IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
    {
        // Avoid computing the operations on the UI thread
        await TaskScheduler.Default;
        return await CodeAction.GetOperationsAsync(this.OriginalSolution, progressTracker, cancellationToken).ConfigureAwait(false);
    }
 
    protected async Task<IEnumerable<CodeActionOperation>> GetOperationsAsync(
        CodeActionWithOptions actionWithOptions, object options, IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
    {
        // Avoid computing the operations on the UI thread
        await TaskScheduler.Default;
        return await actionWithOptions.GetOperationsAsync(this.OriginalSolution, options, progressTracker, cancellationToken).ConfigureAwait(false);
    }
 
    protected async Task<ImmutableArray<CodeActionOperation>> GetPreviewOperationsAsync(CancellationToken cancellationToken)
    {
        // Avoid computing the operations on the UI thread
        await TaskScheduler.Default;
        return await CodeAction.GetPreviewOperationsAsync(this.OriginalSolution, cancellationToken).ConfigureAwait(false);
    }
 
    public void Invoke(CancellationToken cancellationToken)
    {
        throw new NotImplementedException("Invoke(CancellationToken) is no longer supported. Use Invoke(IUIThreadOperationContext) instead.");
    }
 
    public void Invoke(IUIThreadOperationContext context)
    {
        // we're going to return immediately from Invoke and kick off our own async work to invoke the
        // code action. Once this returns, the editor will close the threaded wait dialog it created.
        // So we need to take ownership of it and start our own TWD instead to track this.
        context.TakeOwnership();
 
        _ = InvokeAsync();
    }
 
    private async Task InvokeAsync()
    {
        try
        {
            using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.SuggestedAction_Application_Summary, $"Total");
 
            using var token = SourceProvider.OperationListener.BeginAsyncOperation($"{nameof(SuggestedAction)}.{nameof(Invoke)}");
            using var context = SourceProvider.UIThreadOperationExecutor.BeginExecute(
                EditorFeaturesResources.Execute_Suggested_Action, CodeAction.Title, allowCancellation: true, showProgress: true);
            using var scope = context.AddScope(allowCancellation: true, CodeAction.Message);
            await this.InnerInvokeAsync(scope.GetCodeAnalysisProgress(), context.UserCancellationToken).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
        }
        catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical))
        {
        }
    }
 
    protected virtual async Task InnerInvokeAsync(IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
    {
        await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
        using (new CaretPositionRestorer(SubjectBuffer, EditHandler.AssociatedViewService))
        {
            // ConfigureAwait(true) so that CaretPositionRestorer.Dispose runs on the UI thread.
            await Workspace.Services.GetService<IExtensionManager>().PerformActionAsync(
                Provider, () => InvokeWorkerAsync(progressTracker, cancellationToken)).ConfigureAwait(true);
        }
    }
 
    private async Task InvokeWorkerAsync(IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
    {
        await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
        IEnumerable<CodeActionOperation> operations = null;
        if (CodeAction is CodeActionWithOptions actionWithOptions)
        {
            var options = actionWithOptions.GetOptions(cancellationToken);
            if (options != null)
                operations = await GetOperationsAsync(actionWithOptions, options, progressTracker, cancellationToken).ConfigureAwait(true);
        }
        else
        {
            operations = await GetOperationsAsync(progressTracker, cancellationToken).ConfigureAwait(true);
        }
 
        this.ThreadingContext.ThrowIfNotOnUIThread();
 
        if (operations != null)
        {
            // Clear the progress we showed while computing the action.
            // We'll now show progress as we apply the action.
            progressTracker.Report(CodeAnalysisProgress.Clear());
            progressTracker.Report(CodeAnalysisProgress.Description(EditorFeaturesResources.Applying_changes));
 
            using (Logger.LogBlock(
                FunctionId.CodeFixes_ApplyChanges, KeyValueLogMessage.Create(
                    LogType.UserAction, static (m, @this) => @this.CreateLogProperties(m), this), cancellationToken))
            {
                var document = this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
 
                await EditHandler.ApplyAsync(
                    Workspace,
                    OriginalSolution,
                    document,
                    [.. operations],
                    CodeAction.Title,
                    progressTracker,
                    cancellationToken).ConfigureAwait(false);
            }
        }
    }
 
    private void CreateLogProperties(Dictionary<string, object> map)
    {
        // set various correlation info
        if (CodeAction is AbstractFixAllCodeFixCodeAction fixSome)
        {
            // fix all correlation info
            map[FixAllLogger.CorrelationId] = fixSome.FixAllState.CorrelationId;
            map[FixAllLogger.FixAllScope] = fixSome.FixAllState.Scope.ToString();
        }
 
        if (TryGetTelemetryId(out var telemetryId))
        {
            // Lightbulb correlation info
            map["TelemetryId"] = telemetryId.ToString();
        }
 
        if (this is ITelemetryDiagnosticID<string> diagnosticId)
        {
            // save what it is actually fixing
            map["DiagnosticId"] = diagnosticId.GetDiagnosticID();
        }
    }
 
    public string DisplayText
    {
        get
        {
            // Underscores will become an accelerator in the VS smart tag.  So we double all
            // underscores so they actually get represented as an underscore in the UI.
            var extensionManager = Workspace.Services.GetService<IExtensionManager>();
            var text = extensionManager.PerformFunction(Provider, () => CodeAction.Title, defaultValue: string.Empty);
            return text.Replace("_", "__");
        }
    }
 
    public string DisplayTextSuffix => "";
 
    protected async Task<SolutionPreviewResult> GetPreviewResultAsync(CancellationToken cancellationToken)
    {
        var operations = await GetPreviewOperationsAsync(cancellationToken).ConfigureAwait(true);
 
        await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
        return await EditHandler.GetPreviewsAsync(Workspace, operations, cancellationToken).ConfigureAwait(true);
    }
 
    public virtual bool HasPreview => false;
 
    public virtual Task<object> GetPreviewAsync(CancellationToken cancellationToken)
        => SpecializedTasks.Null<object>();
 
    public virtual bool HasActionSets => false;
 
    public virtual Task<IEnumerable<SuggestedActionSet>> GetActionSetsAsync(CancellationToken cancellationToken)
        => SpecializedTasks.EmptyEnumerable<SuggestedActionSet>();
 
    #region not supported
 
    void IDisposable.Dispose()
    {
        // do nothing
    }
 
    // same as display text
    string ISuggestedAction.IconAutomationText => DisplayText;
 
    ImageMoniker ISuggestedAction.IconMoniker
    {
        get
        {
            var tags = CodeAction.Tags;
            if (tags.Length > 0)
            {
                foreach (var service in SourceProvider.ImageIdServices)
                {
                    if (service.Value.TryGetImageId(tags, out var imageId) && !imageId.Equals(default(ImageId)))
                    {
                        // Not using the extension method because it's not available in Cocoa
                        return new ImageMoniker
                        {
                            Guid = imageId.Guid,
                            Id = imageId.Id
                        };
                    }
                }
            }
 
            return default;
        }
    }
 
    // no shortcut support
    string ISuggestedAction.InputGestureText => null;
 
    #endregion
 
    #region IEquatable<ISuggestedAction>
 
    public bool Equals(ISuggestedAction other)
        => Equals(other as SuggestedAction);
 
    public override bool Equals(object obj)
        => Equals(obj as SuggestedAction);
 
    internal bool Equals(SuggestedAction otherSuggestedAction)
    {
        if (otherSuggestedAction == null)
        {
            return false;
        }
 
        if (this == otherSuggestedAction)
        {
            return true;
        }
 
        if (Provider != otherSuggestedAction.Provider)
        {
            return false;
        }
 
        var otherCodeAction = otherSuggestedAction.CodeAction;
        if (CodeAction.EquivalenceKey == null || otherCodeAction.EquivalenceKey == null)
        {
            return false;
        }
 
        return CodeAction.EquivalenceKey == otherCodeAction.EquivalenceKey;
    }
 
    public override int GetHashCode()
    {
        if (CodeAction.EquivalenceKey == null)
        {
            return base.GetHashCode();
        }
 
        return Hash.Combine(Provider.GetHashCode(), CodeAction.EquivalenceKey.GetHashCode());
    }
 
    #endregion
}