// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Imaging.Interop;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.Implementation.Suggestions
{
///
/// Base class for all Roslyn light bulb menu items.
///
internal partial class SuggestedAction : ForegroundThreadAffinitizedObject, ISuggestedAction, IEquatable
{
protected readonly IAsynchronousOperationListener OperationListener;
protected readonly Workspace Workspace;
protected readonly ITextBuffer SubjectBuffer;
protected readonly ICodeActionEditHandlerService EditHandler;
protected readonly object Provider;
internal readonly CodeAction CodeAction;
private readonly ImmutableArray _actionSets;
protected readonly IWaitIndicator WaitIndicator;
internal SuggestedAction(
Workspace workspace,
ITextBuffer subjectBuffer,
ICodeActionEditHandlerService editHandler,
IWaitIndicator waitIndicator,
CodeAction codeAction,
object provider,
IAsynchronousOperationListener operationListener,
IEnumerable actionSets = null)
{
Contract.ThrowIfTrue(provider == null);
this.Workspace = workspace;
this.SubjectBuffer = subjectBuffer;
this.CodeAction = codeAction;
this.EditHandler = editHandler;
this.WaitIndicator = waitIndicator;
this.Provider = provider;
OperationListener = operationListener;
_actionSets = actionSets.AsImmutableOrEmpty();
}
internal virtual CodeActionPriority Priority => CodeAction?.Priority ?? CodeActionPriority.Medium;
public bool TryGetTelemetryId(out Guid telemetryId)
{
// TODO: this is temporary. Diagnostic team needs to figure out how to provide unique id per a fix.
// for now, we will use type of CodeAction, but there are some predefined code actions that are used by multiple fixes
// and this will not distinguish those
// AssemblyQualifiedName will change across version numbers, FullName won't
var type = CodeAction.GetType();
type = type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type;
telemetryId = new Guid(type.FullName.GetHashCode(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
return true;
}
// NOTE: We want to avoid computing the operations on the UI thread. So we use Task.Run() to do this work on the background thread.
protected Task> GetOperationsAsync(
IProgressTracker progressTracker, CancellationToken cancellationToken)
{
return Task.Run(
() => CodeAction.GetOperationsAsync(progressTracker, cancellationToken), cancellationToken);
}
protected Task> GetOperationsAsync(CodeActionWithOptions actionWithOptions, object options, CancellationToken cancellationToken)
{
return Task.Run(
() => actionWithOptions.GetOperationsAsync(options, cancellationToken), cancellationToken);
}
protected Task> GetPreviewOperationsAsync(CancellationToken cancellationToken)
{
return Task.Run(
() => CodeAction.GetPreviewOperationsAsync(cancellationToken), cancellationToken);
}
public void Invoke(CancellationToken cancellationToken)
{
this.AssertIsForeground();
// Create a task to do the actual async invocation of this action.
// For testing purposes mark that we still have an outstanding async
// operation so that we don't try to validate things too soon.
var asyncToken = OperationListener.BeginAsyncOperation(GetType().Name + "." + nameof(Invoke));
var task = YieldThenInvokeAsync(cancellationToken);
task.CompletesAsyncOperation(asyncToken);
}
private async Task YieldThenInvokeAsync(CancellationToken cancellationToken)
{
this.AssertIsForeground();
// Always wrap whatever we're doing in a threaded wait dialog.
using (var context = this.WaitIndicator.StartWait(CodeAction.Title, CodeAction.Message, allowCancel: true, showProgress: true))
using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, context.CancellationToken))
{
// Yield the UI thread so that the light bulb can be dismissed. This is necessary
// as some code actions may be long running, and we don't want the light bulb to
// stay on screen.
await Task.Yield();
this.AssertIsForeground();
// Then proceed and actually do the invoke.
await InvokeAsync(context.ProgressTracker, linkedSource.Token).ConfigureAwait(true);
}
}
protected virtual async Task InvokeAsync(
IProgressTracker progressTracker, CancellationToken cancellationToken)
{
this.AssertIsForeground();
var snapshot = this.SubjectBuffer.CurrentSnapshot;
using (new CaretPositionRestorer(this.SubjectBuffer, this.EditHandler.AssociatedViewService))
{
Func getFromDocument = () => this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
await InvokeCoreAsync(getFromDocument, progressTracker, cancellationToken).ConfigureAwait(true);
}
}
protected async Task InvokeCoreAsync(
Func getFromDocument, IProgressTracker progressTracker, CancellationToken cancellationToken)
{
this.AssertIsForeground();
var extensionManager = this.Workspace.Services.GetService();
await extensionManager.PerformActionAsync(Provider, async () =>
{
await InvokeWorkerAsync(getFromDocument, progressTracker, cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(true);
}
private async Task InvokeWorkerAsync(
Func getFromDocument, IProgressTracker progressTracker, CancellationToken cancellationToken)
{
this.AssertIsForeground();
IEnumerable operations = null;
// NOTE: As mentioned above, we want to avoid computing the operations on the UI thread.
// However, for CodeActionWithOptions, GetOptions() might involve spinning up a dialog
// to compute the options and must be done on the UI thread.
var actionWithOptions = this.CodeAction as CodeActionWithOptions;
if (actionWithOptions != null)
{
var options = actionWithOptions.GetOptions(cancellationToken);
if (options != null)
{
// ConfigureAwait(true) so we come back to the same thread as
// we do all application on the UI thread.
operations = await GetOperationsAsync(actionWithOptions, options, cancellationToken).ConfigureAwait(true);
this.AssertIsForeground();
}
}
else
{
// ConfigureAwait(true) so we come back to the same thread as
// we do all application on the UI thread.
operations = await GetOperationsAsync(progressTracker, cancellationToken).ConfigureAwait(true);
this.AssertIsForeground();
}
if (operations != null)
{
// Clear the progress we showed while computing the action.
// We'll now show progress as we apply the action.
progressTracker.Clear();
// ConfigureAwait(true) so we come back to the same thread as
// we do all application on the UI thread.
await EditHandler.ApplyAsync(Workspace, getFromDocument(),
operations.ToImmutableArray(), CodeAction.Title,
progressTracker, cancellationToken).ConfigureAwait(true);
}
}
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 = this.Workspace.Services.GetService();
var text = extensionManager.PerformFunction(Provider, () => CodeAction.Title, defaultValue: string.Empty);
return text.Replace("_", "__");
}
}
protected async Task GetPreviewResultAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// We will always invoke this from the UI thread.
AssertIsForeground();
// We use ConfigureAwait(true) to stay on the UI thread.
var operations = await GetPreviewOperationsAsync(cancellationToken).ConfigureAwait(true);
return EditHandler.GetPreviews(Workspace, operations, cancellationToken);
}
public virtual bool HasPreview
{
get
{
// HasPreview is called synchronously on the UI thread. In order to avoid blocking the UI thread,
// we need to provide a 'quick' answer here as opposed to the 'right' answer. Providing the 'right'
// answer is expensive (because we will need to call CodeAction.GetPreviewOperationsAsync() for this
// and this will involve computing the changed solution for the ApplyChangesOperation for the fix /
// refactoring). So we always return 'true' here (so that platform will call GetActionSetsAsync()
// below). Platform guarantees that nothing bad will happen if we return 'true' here and later return
// 'null' / empty collection from within GetPreviewAsync().
return true;
}
}
public virtual async Task