// 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.Editor.Host; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Extensions; 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 Workspace Workspace; protected readonly ITextBuffer SubjectBuffer; protected readonly ICodeActionEditHandlerService EditHandler; protected readonly object Provider; protected readonly CodeAction CodeAction; protected SuggestedAction( Workspace workspace, ITextBuffer subjectBuffer, ICodeActionEditHandlerService editHandler, CodeAction codeAction, object provider) { Contract.ThrowIfTrue(provider == null); this.Workspace = workspace; this.SubjectBuffer = subjectBuffer; this.CodeAction = codeAction; this.EditHandler = editHandler; this.Provider = provider; } 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(CancellationToken cancellationToken) { return Task.Run( async () => await CodeAction.GetOperationsAsync(cancellationToken).ConfigureAwait(false), cancellationToken); } protected Task> GetOperationsAsync(CodeActionWithOptions actionWithOptions, object options, CancellationToken cancellationToken) { return Task.Run( async () => await actionWithOptions.GetOperationsAsync(options, cancellationToken).ConfigureAwait(false), cancellationToken); } protected Task> GetPreviewOperationsAsync(CancellationToken cancellationToken) { return Task.Run( async () => await CodeAction.GetPreviewOperationsAsync(cancellationToken).ConfigureAwait(false), cancellationToken); } public virtual void Invoke(CancellationToken cancellationToken) { var snapshot = this.SubjectBuffer.CurrentSnapshot; using (new CaretPositionRestorer(this.SubjectBuffer, this.EditHandler.AssociatedViewService)) { var extensionManager = this.Workspace.Services.GetService(); extensionManager.PerformAction(Provider, () => { IEnumerable operations = null; // NOTE: As mentoned 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) { operations = GetOperationsAsync(actionWithOptions, options, cancellationToken).WaitAndGetResult(cancellationToken); } } else { operations = GetOperationsAsync(cancellationToken).WaitAndGetResult(cancellationToken); } if (operations != null) { var document = this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); EditHandler.Apply(Workspace, document, operations, CodeAction.Title, cancellationToken); } }); } } 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 GetPreviewAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); // Light bulb will always invoke this function on the UI thread. AssertIsForeground(); var preferredDocumentId = Workspace.GetDocumentIdInCurrentContext(SubjectBuffer.AsTextContainer()); var preferredProjectId = preferredDocumentId?.ProjectId; var extensionManager = this.Workspace.Services.GetService(); var previewContent = await extensionManager.PerformFunctionAsync(Provider, async () => { // We need to stay on UI thread after GetPreviewResultAsync() so that TakeNextPreviewAsync() // below can execute on UI thread. We use ConfigureAwait(true) to stay on the UI thread. var previewResult = await GetPreviewResultAsync(cancellationToken).ConfigureAwait(true); if (previewResult == null) { return null; } else { // TakeNextPreviewAsync() needs to run on UI thread. AssertIsForeground(); return await previewResult.TakeNextPreviewAsync(preferredDocumentId, preferredProjectId, cancellationToken).ConfigureAwait(true); } // GetPreviewPane() below needs to run on UI thread. We use ConfigureAwait(true) to stay on the UI thread. }, defaultValue: null).ConfigureAwait(true); var previewPaneService = Workspace.Services.GetService(); if (previewPaneService == null) { return null; } cancellationToken.ThrowIfCancellationRequested(); // GetPreviewPane() needs to run on the UI thread. AssertIsForeground(); string language; string projectType; Workspace.GetLanguageAndProjectType(preferredProjectId, out language, out projectType); return previewPaneService.GetPreviewPane(GetDiagnostic(), language, projectType, previewContent); } protected virtual Diagnostic GetDiagnostic() { return null; } #region not supported void IDisposable.Dispose() { // do nothing } public virtual bool HasActionSets { get { return false; } } public virtual Task> GetActionSetsAsync(CancellationToken cancellationToken) { return SpecializedTasks.Default>(); } string ISuggestedAction.IconAutomationText { get { // same as display text return DisplayText; } } ImageMoniker ISuggestedAction.IconMoniker { get { // no icon support return default(ImageMoniker); } } string ISuggestedAction.InputGestureText { get { // no shortcut support return null; } } #endregion #region IEquatable public bool Equals(ISuggestedAction other) { return Equals(other as SuggestedAction); } public override bool Equals(object obj) { return Equals(obj as SuggestedAction); } public bool Equals(SuggestedAction otherSuggestedAction) { if (otherSuggestedAction == null) { return false; } if (ReferenceEquals(this, otherSuggestedAction)) { return true; } if (!ReferenceEquals(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 } }