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

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

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

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

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

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

78
        // 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.
79 80
        protected Task<ImmutableArray<CodeActionOperation>> GetOperationsAsync(
            IProgressTracker progressTracker, CancellationToken cancellationToken)
81 82
        {
            return Task.Run(
83
                () => CodeAction.GetOperationsAsync(progressTracker, cancellationToken), cancellationToken);
84 85 86 87 88
        }

        protected Task<IEnumerable<CodeActionOperation>> GetOperationsAsync(CodeActionWithOptions actionWithOptions, object options, CancellationToken cancellationToken)
        {
            return Task.Run(
89
                () => actionWithOptions.GetOperationsAsync(options, cancellationToken), cancellationToken);
90 91 92 93 94
        }

        protected Task<ImmutableArray<CodeActionOperation>> GetPreviewOperationsAsync(CancellationToken cancellationToken)
        {
            return Task.Run(
95
                () => CodeAction.GetPreviewOperationsAsync(cancellationToken), cancellationToken);
96 97
        }

98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
        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.
115
            using (var context = this.WaitIndicator.StartWait(CodeAction.Title, CodeAction.Message, allowCancel: true, showProgress: true))
116 117
            using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, context.CancellationToken))
            {
118 119 120 121 122
                // 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();

123 124 125
                this.AssertIsForeground();

                // Then proceed and actually do the invoke.
126
                await InvokeAsync(context.ProgressTracker, linkedSource.Token).ConfigureAwait(true);
127 128 129
            }
        }

130 131
        protected virtual async Task InvokeAsync( 
            IProgressTracker progressTracker, CancellationToken cancellationToken)
132
        {
133 134
            this.AssertIsForeground();

135 136 137 138
            var snapshot = this.SubjectBuffer.CurrentSnapshot;

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

144 145
        protected async Task InvokeCoreAsync(
            Func<Document> getFromDocument, IProgressTracker progressTracker, CancellationToken cancellationToken)
146
        {
147 148
            this.AssertIsForeground();

149
            var extensionManager = this.Workspace.Services.GetService<IExtensionManager>();
150
            await extensionManager.PerformActionAsync(Provider, async () =>
151
            {
152
                await InvokeWorkerAsync(getFromDocument, progressTracker, cancellationToken).ConfigureAwait(false);
153
            }).ConfigureAwait(true);
154 155
        }

156 157
        private async Task InvokeWorkerAsync(
            Func<Document> getFromDocument, IProgressTracker progressTracker, CancellationToken cancellationToken)
158
        {
159
            this.AssertIsForeground();
160
            IEnumerable<CodeActionOperation> operations = null;
161

162 163 164 165 166 167 168 169
            // 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)
170
                {
171 172
                    // ConfigureAwait(true) so we come back to the same thread as 
                    // we do all application on the UI thread.
173 174
                    operations = await GetOperationsAsync(actionWithOptions, options, cancellationToken).ConfigureAwait(true);
                    this.AssertIsForeground();
175
                }
176 177 178
            }
            else
            {
179 180
                // ConfigureAwait(true) so we come back to the same thread as 
                // we do all application on the UI thread.
181
                operations = await GetOperationsAsync(progressTracker, cancellationToken).ConfigureAwait(true);
182
                this.AssertIsForeground();
183 184 185 186
            }

            if (operations != null)
            {
187 188 189
                // Clear the progress we showed while computing the action.
                // We'll now show progress as we apply the action.
                progressTracker.Clear();
190 191 192

                // ConfigureAwait(true) so we come back to the same thread as 
                // we do all application on the UI thread.
193 194
                await EditHandler.ApplyAsync(Workspace, getFromDocument(), 
                    operations.ToImmutableArray(), CodeAction.Title, 
195
                    progressTracker, cancellationToken).ConfigureAwait(true);
196
            }
197 198 199 200 201 202 203 204 205
        }

        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 已提交
206
                var text = extensionManager.PerformFunction(Provider, () => CodeAction.Title, defaultValue: string.Empty);
207 208 209 210
                return text.Replace("_", "__");
            }
        }

211
        protected async Task<SolutionPreviewResult> GetPreviewResultAsync(CancellationToken cancellationToken)
212
        {
213
            cancellationToken.ThrowIfCancellationRequested();
214

215 216 217 218 219 220 221 222
            // 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);
        }
223

224 225 226 227 228 229
        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 已提交
230
                // answer is expensive (because we will need to call CodeAction.GetPreviewOperationsAsync() for this
231 232 233 234 235 236
                // 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 已提交
237
            }
238 239
        }

240
        public virtual async Task<object> GetPreviewAsync(CancellationToken cancellationToken)
241
        {
242 243 244 245 246
            cancellationToken.ThrowIfCancellationRequested();

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

247 248 249 250 251 252 253 254
            var previewPaneService = Workspace.Services.GetService<IPreviewPaneService>();
            if (previewPaneService == null)
            {
                return null;
            }

            // after this point, this method should only return at GetPreviewPane. otherwise, DifferenceViewer will leak
            // since there is no one to close the viewer
255
            var preferredDocumentId = Workspace.GetDocumentIdInCurrentContext(SubjectBuffer.AsTextContainer());
H
Heejae Chang 已提交
256
            var preferredProjectId = preferredDocumentId?.ProjectId;
257

258
            var extensionManager = this.Workspace.Services.GetService<IExtensionManager>();
259
            var previewContents = await extensionManager.PerformFunctionAsync(Provider, async () =>
260
            {
261 262 263
                // 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);
264 265 266 267 268 269
                if (previewResult == null)
                {
                    return null;
                }
                else
                {
270 271
                    // TakeNextPreviewAsync() needs to run on UI thread.
                    AssertIsForeground();
272
                    return await previewResult.GetPreviewsAsync(preferredDocumentId, preferredProjectId, cancellationToken).ConfigureAwait(true);
273 274
                }

275
                // GetPreviewPane() below needs to run on UI thread. We use ConfigureAwait(true) to stay on the UI thread.
S
Shyam N 已提交
276
            }, defaultValue: null).ConfigureAwait(true);
277

278 279
            // GetPreviewPane() needs to run on the UI thread.
            AssertIsForeground();
280 281 282

            string language;
            string projectType;
H
Heejae Chang 已提交
283
            Workspace.GetLanguageAndProjectType(preferredProjectId, out language, out projectType);
284

285
            return previewPaneService.GetPreviewPane(GetDiagnostic(), language, projectType, previewContents);
286 287
        }

288
        protected virtual DiagnosticData GetDiagnostic()
289 290 291 292
        {
            return null;
        }

293
        public virtual bool HasActionSets => _actionSets.Length > 0;
294

295
        public virtual Task<IEnumerable<SuggestedActionSet>> GetActionSetsAsync(CancellationToken cancellationToken)
296
        {
297
            return Task.FromResult<IEnumerable<SuggestedActionSet>>(GetActionSets());
S
Shyam N 已提交
298
        }
299

300
        internal ImmutableArray<SuggestedActionSet> GetActionSets()
301
        {
302
            return _actionSets;
303 304
        }

305 306 307
        #region not supported

        void IDisposable.Dispose()
308
        {
309
            // do nothing
310 311
        }

312 313 314
        // same as display text
        string ISuggestedAction.IconAutomationText => DisplayText;

315 316 317 318 319 320 321 322 323 324 325 326 327
        ImageMoniker ISuggestedAction.IconMoniker
        {
            get
            {
                if (CodeAction.Glyph.HasValue)
                {
                    var imageService = Workspace.Services.GetService<IImageMonikerService>();
                    return imageService.GetImageMoniker((Glyph)CodeAction.Glyph.Value);
                }

                return default(ImageMoniker);
            }
        }
328 329 330 331 332 333 334 335 336

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

338 339 340
        #endregion

        #region IEquatable<ISuggestedAction>
341

342 343 344 345 346 347 348 349 350 351
        public bool Equals(ISuggestedAction other)
        {
            return Equals(other as SuggestedAction);
        }

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

352
        internal bool Equals(SuggestedAction otherSuggestedAction)
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
        {
            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());
        }
387

388
        #endregion
C
CyrusNajmabadi 已提交
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408

        protected async Task<SuggestedActionSet> GetPreviewChangesSuggestedActionSetAsync(CancellationToken cancellationToken)
        {
            var previewResult = await GetPreviewResultAsync(cancellationToken).ConfigureAwait(true);
            if (previewResult == null)
            {
                return null;
            }

            var changeSummary = previewResult.ChangeSummary;
            if (changeSummary == null)
            {
                return null;
            }

            var previewAction = new PreviewChangesCodeAction(Workspace, CodeAction, changeSummary);
            var previewSuggestedAction = new PreviewChangesSuggestedAction(
                Workspace, SubjectBuffer, EditHandler, WaitIndicator, previewAction, Provider, OperationListener);
            return new SuggestedActionSet(ImmutableArray.Create(previewSuggestedAction));
        }
409 410
    }
}