SuggestedAction.cs 12.9 KB
Newer Older
1 2 3 4
// 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;
5
using System.Collections.Immutable;
6 7 8
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
9
using Microsoft.CodeAnalysis.Diagnostics;
10
using Microsoft.CodeAnalysis.Editor.Host;
11
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
12
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
13 14
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Text;
15
using Microsoft.VisualStudio.Imaging.Interop;
16 17 18 19 20 21 22 23 24
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.Suggestions
{
    /// <summary>
    /// Base class for all Roslyn light bulb menu items.
    /// </summary>
25
    internal partial class SuggestedAction : ForegroundThreadAffinitizedObject, ISuggestedAction, IEquatable<ISuggestedAction>
26 27 28 29 30 31
    {
        protected readonly Workspace Workspace;
        protected readonly ITextBuffer SubjectBuffer;
        protected readonly ICodeActionEditHandlerService EditHandler;

        protected readonly object Provider;
32 33
        internal readonly CodeAction CodeAction;
        private readonly ImmutableArray<SuggestedActionSet> _actionSets;
34
        protected readonly IWaitIndicator WaitIndicator;
35

36
        internal SuggestedAction(
37 38 39
            Workspace workspace,
            ITextBuffer subjectBuffer,
            ICodeActionEditHandlerService editHandler,
40
            IWaitIndicator waitIndicator,
41
            CodeAction codeAction,
42 43
            object provider,
            IEnumerable<SuggestedActionSet> actionSets = null)
44 45 46 47 48 49 50
        {
            Contract.ThrowIfTrue(provider == null);

            this.Workspace = workspace;
            this.SubjectBuffer = subjectBuffer;
            this.CodeAction = codeAction;
            this.EditHandler = editHandler;
51
            this.WaitIndicator = waitIndicator;
52
            this.Provider = provider;
53
            _actionSets = actionSets.AsImmutableOrEmpty();
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
        }

        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;
        }

70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
        // 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<ImmutableArray<CodeActionOperation>> GetOperationsAsync(CancellationToken cancellationToken)
        {
            return Task.Run(
                async () => await CodeAction.GetOperationsAsync(cancellationToken).ConfigureAwait(false), cancellationToken);
        }

        protected Task<IEnumerable<CodeActionOperation>> GetOperationsAsync(CodeActionWithOptions actionWithOptions, object options, CancellationToken cancellationToken)
        {
            return Task.Run(
                async () => await actionWithOptions.GetOperationsAsync(options, cancellationToken).ConfigureAwait(false), cancellationToken);
        }

        protected Task<ImmutableArray<CodeActionOperation>> GetPreviewOperationsAsync(CancellationToken cancellationToken)
        {
            return Task.Run(
                async () => await CodeAction.GetPreviewOperationsAsync(cancellationToken).ConfigureAwait(false), cancellationToken);
        }

89 90
        public virtual void Invoke(CancellationToken cancellationToken)
        {
91 92
            this.AssertIsForeground();

93 94 95 96
            var snapshot = this.SubjectBuffer.CurrentSnapshot;

            using (new CaretPositionRestorer(this.SubjectBuffer, this.EditHandler.AssociatedViewService))
            {
97 98 99 100
                Func<Document> getFromDocument = () => this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
                InvokeCore(getFromDocument, cancellationToken);
            }
        }
101

102 103
        public void InvokeCore(Func<Document> getFromDocument, CancellationToken cancellationToken)
        {
104 105
            this.AssertIsForeground();

106 107 108
            var extensionManager = this.Workspace.Services.GetService<IExtensionManager>();
            extensionManager.PerformAction(Provider, () =>
            {
109
                this.WaitIndicator.Wait(CodeAction.Title, CodeAction.Message, allowCancel: true, action: context =>
110
                {
C
Cyrus Najmabadi 已提交
111 112 113 114
                    using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, context.CancellationToken))
                    {
                        InvokeWorker(getFromDocument, linkedSource.Token);
                    }
115 116 117 118 119 120 121
                });
            });
        }

        private void InvokeWorker(Func<Document> getFromDocument, CancellationToken cancellationToken)
        {
            IEnumerable<CodeActionOperation> operations = null;
122

123 124 125 126 127 128 129 130
            // 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)
131
                {
132
                    operations = GetOperationsAsync(actionWithOptions, options, cancellationToken).WaitAndGetResult(cancellationToken);
133
                }
134 135 136 137 138 139 140 141 142 143
            }
            else
            {
                operations = GetOperationsAsync(cancellationToken).WaitAndGetResult(cancellationToken);
            }

            if (operations != null)
            {
                EditHandler.Apply(Workspace, getFromDocument(), operations, CodeAction.Title, cancellationToken);
            }
144 145 146 147 148 149 150 151 152
        }

        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<IExtensionManager>();
S
Shyam N 已提交
153
                var text = extensionManager.PerformFunction(Provider, () => CodeAction.Title, defaultValue: string.Empty);
154 155 156 157
                return text.Replace("_", "__");
            }
        }

158
        protected async Task<SolutionPreviewResult> GetPreviewResultAsync(CancellationToken cancellationToken)
159
        {
160
            cancellationToken.ThrowIfCancellationRequested();
161

162 163 164 165 166 167 168 169
            // 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);
        }
170

171 172 173 174 175 176
        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'
C
Charles Stoner 已提交
177
                // answer is expensive (because we will need to call CodeAction.GetPreviewOperationsAsync() for this
178 179 180 181 182 183
                // 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;
S
Shyam N 已提交
184
            }
185 186
        }

187
        public virtual async Task<object> GetPreviewAsync(CancellationToken cancellationToken)
188
        {
189 190 191 192 193
            cancellationToken.ThrowIfCancellationRequested();

            // Light bulb will always invoke this function on the UI thread.
            AssertIsForeground();

194
            var preferredDocumentId = Workspace.GetDocumentIdInCurrentContext(SubjectBuffer.AsTextContainer());
H
Heejae Chang 已提交
195
            var preferredProjectId = preferredDocumentId?.ProjectId;
196

197
            var extensionManager = this.Workspace.Services.GetService<IExtensionManager>();
198
            var previewContent = await extensionManager.PerformFunctionAsync(Provider, async () =>
199
            {
200 201 202
                // 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);
203 204 205 206 207 208
                if (previewResult == null)
                {
                    return null;
                }
                else
                {
209 210
                    // TakeNextPreviewAsync() needs to run on UI thread.
                    AssertIsForeground();
211
                    return await previewResult.GetPreviewsAsync(preferredDocumentId, preferredProjectId, cancellationToken).ConfigureAwait(true);
212 213
                }

214
                // GetPreviewPane() below needs to run on UI thread. We use ConfigureAwait(true) to stay on the UI thread.
S
Shyam N 已提交
215
            }, defaultValue: null).ConfigureAwait(true);
216 217 218 219

            var previewPaneService = Workspace.Services.GetService<IPreviewPaneService>();
            if (previewPaneService == null)
            {
220
                return null;
221 222
            }

223 224 225 226
            cancellationToken.ThrowIfCancellationRequested();

            // GetPreviewPane() needs to run on the UI thread.
            AssertIsForeground();
227 228 229

            string language;
            string projectType;
H
Heejae Chang 已提交
230
            Workspace.GetLanguageAndProjectType(preferredProjectId, out language, out projectType);
231 232

            return previewPaneService.GetPreviewPane(GetDiagnostic(), language, projectType, previewContent);
233 234
        }

235
        protected virtual DiagnosticData GetDiagnostic()
236 237 238 239
        {
            return null;
        }

240
        public virtual bool HasActionSets => _actionSets.Length > 0;
241

242
        public virtual Task<IEnumerable<SuggestedActionSet>> GetActionSetsAsync(CancellationToken cancellationToken)
243
        {
244
            return Task.FromResult<IEnumerable<SuggestedActionSet>>(GetActionSets());
S
Shyam N 已提交
245
        }
246

247
        internal ImmutableArray<SuggestedActionSet> GetActionSets()
248
        {
249
            return _actionSets;
250 251
        }

252 253 254
        #region not supported

        void IDisposable.Dispose()
255
        {
256
            // do nothing
257 258
        }

259 260 261
        // same as display text
        string ISuggestedAction.IconAutomationText => DisplayText;

C
CyrusNajmabadi 已提交
262
        ImageMoniker ISuggestedAction.IconMoniker => CodeAction.Glyph?.GetImageMoniker() ?? default(ImageMoniker);
263 264 265 266 267 268 269 270 271

        string ISuggestedAction.InputGestureText
        {
            get
            {
                // no shortcut support
                return null;
            }
        }
272

273 274 275
        #endregion

        #region IEquatable<ISuggestedAction>
276

277 278 279 280 281 282 283 284 285 286
        public bool Equals(ISuggestedAction other)
        {
            return Equals(other as SuggestedAction);
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as SuggestedAction);
        }

287
        internal bool Equals(SuggestedAction otherSuggestedAction)
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
        {
            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());
        }
322

323 324 325
        #endregion
    }
}