SuggestedAction.cs 14.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;
12
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
13
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
14
using Microsoft.CodeAnalysis.Extensions;
15
using Microsoft.CodeAnalysis.Shared.TestHooks;
16
using Microsoft.CodeAnalysis.Text;
17
using Microsoft.VisualStudio.Imaging.Interop;
18 19 20 21 22 23 24 25 26
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>
27
    internal partial class SuggestedAction : ForegroundThreadAffinitizedObject, ISuggestedAction, IEquatable<ISuggestedAction>
28
    {
29
        protected readonly IAsynchronousOperationListener OperationListener;
30 31 32 33 34
        protected readonly Workspace Workspace;
        protected readonly ITextBuffer SubjectBuffer;
        protected readonly ICodeActionEditHandlerService EditHandler;

        protected readonly object Provider;
35 36
        internal readonly CodeAction CodeAction;
        private readonly ImmutableArray<SuggestedActionSet> _actionSets;
37
        protected readonly IWaitIndicator WaitIndicator;
38

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

            this.Workspace = workspace;
            this.SubjectBuffer = subjectBuffer;
            this.CodeAction = codeAction;
            this.EditHandler = editHandler;
55
            this.WaitIndicator = waitIndicator;
56
            this.Provider = provider;
57
            OperationListener = operationListener;
58
            _actionSets = actionSets.AsImmutableOrEmpty();
59 60
        }

61 62
        internal virtual CodeActionPriority Priority => CodeAction?.Priority ?? CodeActionPriority.Medium;

63 64 65 66 67 68 69 70 71 72 73 74 75 76
        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;
        }

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
        // 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);
        }

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
        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();

            // 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();

            // Always wrap whatever we're doing in a threaded wait dialog.
            using (var context = this.WaitIndicator.StartWait(CodeAction.Title, CodeAction.Message, allowCancel: true))
            using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, context.CancellationToken))
            {
                this.AssertIsForeground();

                // Then proceed and actually do the invoke.
                await InvokeAsync(linkedSource.Token).ConfigureAwait(true);
            }
        }

        protected virtual async Task InvokeAsync(CancellationToken cancellationToken)
129
        {
130 131
            this.AssertIsForeground();

132 133 134 135
            var snapshot = this.SubjectBuffer.CurrentSnapshot;

            using (new CaretPositionRestorer(this.SubjectBuffer, this.EditHandler.AssociatedViewService))
            {
136
                Func<Document> getFromDocument = () => this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
137
                await InvokeCoreAsync(getFromDocument, cancellationToken).ConfigureAwait(true);
138 139
            }
        }
140

141
        protected async Task InvokeCoreAsync(Func<Document> getFromDocument, CancellationToken cancellationToken)
142
        {
143 144
            this.AssertIsForeground();

145
            var extensionManager = this.Workspace.Services.GetService<IExtensionManager>();
146
            await extensionManager.PerformActionAsync(Provider, async () =>
147
            {
148 149
                await InvokeWorkerAsync(getFromDocument, cancellationToken).ConfigureAwait(false);
            }).ConfigureAwait(true);
150 151
        }

152
        private async Task InvokeWorkerAsync(Func<Document> getFromDocument, CancellationToken cancellationToken)
153
        {
154
            this.AssertIsForeground();
155
            IEnumerable<CodeActionOperation> operations = null;
156

157 158 159 160 161 162 163 164
            // 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)
165
                {
166 167
                    operations = await GetOperationsAsync(actionWithOptions, options, cancellationToken).ConfigureAwait(true);
                    this.AssertIsForeground();
168
                }
169 170 171
            }
            else
            {
172 173
                operations = await GetOperationsAsync(cancellationToken).ConfigureAwait(true);
                this.AssertIsForeground();
174 175 176 177 178 179
            }

            if (operations != null)
            {
                EditHandler.Apply(Workspace, getFromDocument(), operations, CodeAction.Title, cancellationToken);
            }
180 181 182 183 184 185 186 187 188
        }

        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 已提交
189
                var text = extensionManager.PerformFunction(Provider, () => CodeAction.Title, defaultValue: string.Empty);
190 191 192 193
                return text.Replace("_", "__");
            }
        }

194
        protected async Task<SolutionPreviewResult> GetPreviewResultAsync(CancellationToken cancellationToken)
195
        {
196
            cancellationToken.ThrowIfCancellationRequested();
197

198 199 200 201 202 203 204 205
            // 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);
        }
206

207 208 209 210 211 212
        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 已提交
213
                // answer is expensive (because we will need to call CodeAction.GetPreviewOperationsAsync() for this
214 215 216 217 218 219
                // 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 已提交
220
            }
221 222
        }

223
        public virtual async Task<object> GetPreviewAsync(CancellationToken cancellationToken)
224
        {
225 226 227 228 229
            cancellationToken.ThrowIfCancellationRequested();

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

230
            var preferredDocumentId = Workspace.GetDocumentIdInCurrentContext(SubjectBuffer.AsTextContainer());
H
Heejae Chang 已提交
231
            var preferredProjectId = preferredDocumentId?.ProjectId;
232

233
            var extensionManager = this.Workspace.Services.GetService<IExtensionManager>();
234
            var previewContent = await extensionManager.PerformFunctionAsync(Provider, async () =>
235
            {
236 237 238
                // 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);
239 240 241 242 243 244
                if (previewResult == null)
                {
                    return null;
                }
                else
                {
245 246
                    // TakeNextPreviewAsync() needs to run on UI thread.
                    AssertIsForeground();
247
                    return await previewResult.GetPreviewsAsync(preferredDocumentId, preferredProjectId, cancellationToken).ConfigureAwait(true);
248 249
                }

250
                // GetPreviewPane() below needs to run on UI thread. We use ConfigureAwait(true) to stay on the UI thread.
S
Shyam N 已提交
251
            }, defaultValue: null).ConfigureAwait(true);
252 253 254 255

            var previewPaneService = Workspace.Services.GetService<IPreviewPaneService>();
            if (previewPaneService == null)
            {
256
                return null;
257 258
            }

259 260 261 262
            cancellationToken.ThrowIfCancellationRequested();

            // GetPreviewPane() needs to run on the UI thread.
            AssertIsForeground();
263 264 265

            string language;
            string projectType;
H
Heejae Chang 已提交
266
            Workspace.GetLanguageAndProjectType(preferredProjectId, out language, out projectType);
267 268

            return previewPaneService.GetPreviewPane(GetDiagnostic(), language, projectType, previewContent);
269 270
        }

271
        protected virtual DiagnosticData GetDiagnostic()
272 273 274 275
        {
            return null;
        }

276
        public virtual bool HasActionSets => _actionSets.Length > 0;
277

278
        public virtual Task<IEnumerable<SuggestedActionSet>> GetActionSetsAsync(CancellationToken cancellationToken)
279
        {
280
            return Task.FromResult<IEnumerable<SuggestedActionSet>>(GetActionSets());
S
Shyam N 已提交
281
        }
282

283
        internal ImmutableArray<SuggestedActionSet> GetActionSets()
284
        {
285
            return _actionSets;
286 287
        }

288 289 290
        #region not supported

        void IDisposable.Dispose()
291
        {
292
            // do nothing
293 294
        }

295 296 297
        // same as display text
        string ISuggestedAction.IconAutomationText => DisplayText;

298 299 300 301 302 303 304 305 306 307 308 309 310
        ImageMoniker ISuggestedAction.IconMoniker
        {
            get
            {
                if (CodeAction.Glyph.HasValue)
                {
                    var imageService = Workspace.Services.GetService<IImageMonikerService>();
                    return imageService.GetImageMoniker((Glyph)CodeAction.Glyph.Value);
                }

                return default(ImageMoniker);
            }
        }
311 312 313 314 315 316 317 318 319

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

321 322 323
        #endregion

        #region IEquatable<ISuggestedAction>
324

325 326 327 328 329 330 331 332 333 334
        public bool Equals(ISuggestedAction other)
        {
            return Equals(other as SuggestedAction);
        }

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

335
        internal bool Equals(SuggestedAction otherSuggestedAction)
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
        {
            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());
        }
370

371 372 373
        #endregion
    }
}