SuggestedActionsSourceProvider.cs 36.6 KB
Newer Older
1 2 3 4 5 6
// 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.ComponentModel.Composition;
7
using System.Diagnostics;
8 9 10
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
11
using Microsoft.CodeAnalysis.CodeActions;
12 13 14 15
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeFixes.Suppression;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Diagnostics;
16
using Microsoft.CodeAnalysis.Editor.Host;
17
using Microsoft.CodeAnalysis.Editor.Shared;
18 19 20 21 22 23 24
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Options;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
25
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
26 27 28 29 30 31 32 33
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.Suggestions
{
    [Export(typeof(ISuggestedActionsSourceProvider))]
34
    [VisualStudio.Utilities.ContentType(ContentTypeNames.RoslynContentType)]
35 36 37 38 39 40 41 42 43 44 45 46 47 48
    [VisualStudio.Utilities.Name("Roslyn Code Fix")]
    [VisualStudio.Utilities.Order]
    internal class SuggestedActionsSourceProvider : ISuggestedActionsSourceProvider
    {
        private static readonly Guid s_CSharpSourceGuid = new Guid("b967fea8-e2c3-4984-87d4-71a38f49e16a");
        private static readonly Guid s_visualBasicSourceGuid = new Guid("4de30e93-3e0c-40c2-a4ba-1124da4539f6");

        private const int InvalidSolutionVersion = -1;

        private readonly ICodeRefactoringService _codeRefactoringService;
        private readonly IDiagnosticAnalyzerService _diagnosticService;
        private readonly ICodeFixService _codeFixService;
        private readonly ICodeActionEditHandlerService _editHandler;
        private readonly IAsynchronousOperationListener _listener;
49
        private readonly IWaitIndicator _waitIndicator;
50 51 52 53 54 55 56

        [ImportingConstructor]
        public SuggestedActionsSourceProvider(
            ICodeRefactoringService codeRefactoringService,
            IDiagnosticAnalyzerService diagnosticService,
            ICodeFixService codeFixService,
            ICodeActionEditHandlerService editHandler,
57
            IWaitIndicator waitIndicator,
58 59 60 61 62 63
            [ImportMany] IEnumerable<Lazy<IAsynchronousOperationListener, FeatureMetadata>> asyncListeners)
        {
            _codeRefactoringService = codeRefactoringService;
            _diagnosticService = diagnosticService;
            _codeFixService = codeFixService;
            _editHandler = editHandler;
64
            _waitIndicator = waitIndicator;
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
            _listener = new AggregateAsynchronousOperationListener(asyncListeners, FeatureAttribute.LightBulb);
        }

        public ISuggestedActionsSource CreateSuggestedActionsSource(ITextView textView, ITextBuffer textBuffer)
        {
            Contract.ThrowIfNull(textView);
            Contract.ThrowIfNull(textBuffer);

            return new Source(this, textView, textBuffer);
        }

        private class Source : ForegroundThreadAffinitizedObject, ISuggestedActionsSource
        {
            // state that will be only reset when source is disposed.
            private SuggestedActionsSourceProvider _owner;
            private ITextView _textView;
            private ITextBuffer _subjectBuffer;
            private WorkspaceRegistration _registration;

            // mutable state
            private Workspace _workspace;
            private int _lastSolutionVersionReported;

            public Source(SuggestedActionsSourceProvider owner, ITextView textView, ITextBuffer textBuffer)
            {
                _owner = owner;
                _textView = textView;
                _textView.Closed += OnTextViewClosed;

                _subjectBuffer = textBuffer;
                _registration = Workspace.GetWorkspaceRegistration(textBuffer.AsTextContainer());

                _lastSolutionVersionReported = InvalidSolutionVersion;
                var updateSource = (IDiagnosticUpdateSource)_owner._diagnosticService;
                updateSource.DiagnosticsUpdated += OnDiagnosticsUpdated;

                if (_registration.Workspace != null)
                {
                    _workspace = _registration.Workspace;
                    _workspace.DocumentActiveContextChanged += OnActiveContextChanged;
                }

                _registration.WorkspaceChanged += OnWorkspaceChanged;
            }

            public event EventHandler<EventArgs> SuggestedActionsChanged;

            public bool TryGetTelemetryId(out Guid telemetryId)
            {
                telemetryId = default(Guid);

                var workspace = _workspace;
                if (workspace == null || _subjectBuffer == null)
                {
                    return false;
                }

                var documentId = workspace.GetDocumentIdInCurrentContext(_subjectBuffer.AsTextContainer());
                if (documentId == null)
                {
                    return false;
                }

                var project = workspace.CurrentSolution.GetProject(documentId.ProjectId);
                if (project == null)
                {
                    return false;
                }

                switch (project.Language)
                {
                    case LanguageNames.CSharp:
                        telemetryId = s_CSharpSourceGuid;
                        return true;
                    case LanguageNames.VisualBasic:
                        telemetryId = s_visualBasicSourceGuid;
                        return true;
                    default:
                        return false;
                }
            }

            public IEnumerable<SuggestedActionSet> GetSuggestedActions(ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, CancellationToken cancellationToken)
            {
                AssertIsForeground();

                using (Logger.LogBlock(FunctionId.SuggestedActions_GetSuggestedActions, cancellationToken))
                {
                    var documentAndSnapshot = GetMatchingDocumentAndSnapshotAsync(range.Snapshot, cancellationToken).WaitAndGetResult(cancellationToken);
                    if (!documentAndSnapshot.HasValue)
                    {
                        // this is here to fail test and see why it is failed.
157
                        Trace.WriteLine("given range is not current");
158 159 160 161 162
                        return null;
                    }

                    var document = documentAndSnapshot.Value.Item1;
                    var workspace = document.Project.Solution.Workspace;
163
                    var supportsFeatureService = workspace.Services.GetService<IDocumentSupportsFeatureService>();
164

165 166
                    var fixes = GetCodeFixes(supportsFeatureService, requestedActionCategories, workspace, document, range, cancellationToken);
                    var refactorings = GetRefactorings(supportsFeatureService, requestedActionCategories, workspace, document, range, cancellationToken);
167

168
                    var result = fixes == null ? refactorings : refactorings == null
169 170 171 172 173 174 175 176 177 178
                                               ? fixes : fixes.Concat(refactorings);

                    if (result == null)
                    {
                        return null;
                    }

                    var allActionSets = result.ToList();
                    allActionSets = InlineActionSetsIfDesirable(allActionSets);
                    return allActionSets;
179 180
                }
            }
181

182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
            private List<SuggestedActionSet> InlineActionSetsIfDesirable(List<SuggestedActionSet> allActionSets)
            {
                // If we only have a single set of items, and that set only has three max suggestion 
                // offered.  Then we can consider inlining any nested actions into the top level list.
                // (but we only do this if the parent of the nested actions isn't invokable itself).
                if (allActionSets.Sum(a => a.Actions.Count()) > 3)
                {
                    return allActionSets;
                }

                return allActionSets.Select(InlineActions).ToList();
            }

            private bool IsInlineable(ISuggestedAction action)
            {
                var suggestedAction = action as SuggestedAction;
                return suggestedAction != null &&
                         !suggestedAction.CodeAction.IsInvokable &&
                         suggestedAction.CodeAction.HasCodeActions;
            }

            private SuggestedActionSet InlineActions(SuggestedActionSet actionSet)
            {
                if (!actionSet.Actions.Any(IsInlineable))
                {
                    return actionSet;
                }

                var newActions = new List<ISuggestedAction>();
                foreach (var action in actionSet.Actions)
                {
                    if (IsInlineable(action))
                    {
                        // Looks like something we can inline.
                        var childActionSets = ((SuggestedAction)action).GetActionSets();
                        if (childActionSets.Length != 1)
                        {
                            return actionSet;
                        }

                        newActions.AddRange(childActionSets[0].Actions);
223
                        continue;
224 225 226 227 228 229 230 231
                    }

                    newActions.Add(action);
                }

                return new SuggestedActionSet(newActions, actionSet.Title, actionSet.Priority, actionSet.ApplicableToSpan);
            }

232
            private IEnumerable<SuggestedActionSet> GetCodeFixes(
233
                IDocumentSupportsFeatureService supportsFeatureService,
234 235 236 237 238 239
                ISuggestedActionCategorySet requestedActionCategories,
                Workspace workspace,
                Document document,
                SnapshotSpan range,
                CancellationToken cancellationToken)
            {
240 241
                this.AssertIsForeground();

242
                if (_owner._codeFixService != null && supportsFeatureService.SupportsCodeFixes(document) &&
243 244 245 246 247
                    requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.CodeFix))
                {
                    // We only include suppressions if lightbulb is asking for everything.
                    // If the light bulb is only asking for code fixes, then we don't include suppressions.
                    var includeSuppressionFixes = requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.Any);
248

249
                    var fixes = Task.Run(
250 251 252 253 254 255
                        async () =>
                        {
                            var stream = await _owner._codeFixService.GetFixesAsync(
                                document, range.Span.ToTextSpan(), includeSuppressionFixes, cancellationToken).ConfigureAwait(false);
                            return stream.ToList();
                        },
256
                        cancellationToken).WaitAndGetResult(cancellationToken);
257

258 259 260
                    var filteredFixes = FilterOnUIThread(fixes, workspace);

                    return OrganizeFixes(workspace, filteredFixes, hasSuppressionFixes: includeSuppressionFixes);
261
                }
262 263

                return null;
264 265
            }

266 267 268 269 270 271 272 273 274 275 276
            private List<CodeFixCollection> FilterOnUIThread(List<CodeFixCollection> collections, Workspace workspace)
            {
                this.AssertIsForeground();

                return collections.Select(c => FilterOnUIThread(c, workspace)).WhereNotNull().ToList();
            }

            private CodeFixCollection FilterOnUIThread(CodeFixCollection collection, Workspace workspace)
            {
                this.AssertIsForeground();

C
CyrusNajmabadi 已提交
277
                var applicableFixes = collection.Fixes.Where(f => IsApplicable(f.Action, workspace)).ToList();
278 279 280 281 282 283 284
                return applicableFixes.Count == 0
                    ? null
                    : applicableFixes.Count == collection.Fixes.Length
                        ? collection
                        : new CodeFixCollection(collection.Provider, collection.TextSpan, applicableFixes, collection.FixAllContext);
            }

C
CyrusNajmabadi 已提交
285 286 287 288 289 290 291 292 293 294 295 296 297 298
            private bool IsApplicable(CodeAction action, Workspace workspace)
            {
                if (!action.PerformFinalApplicabilityCheck)
                {
                    // If we don't even need to perform the final applicability check,
                    // then the code actoin is applicable.
                    return true;
                }

                // Otherwise, defer to the action to make the decision.
                this.AssertIsForeground();
                return action.IsApplicable(workspace);
            }

299 300 301 302 303 304 305
            private List<CodeRefactoring> FilterOnUIThread(List<CodeRefactoring> refactorings, Workspace workspace)
            {
                return refactorings.Select(r => FilterOnUIThread(r, workspace)).WhereNotNull().ToList();
            }

            private CodeRefactoring FilterOnUIThread(CodeRefactoring refactoring, Workspace workspace)
            {
C
CyrusNajmabadi 已提交
306
                var actions = refactoring.Actions.Where(a => IsApplicable(a, workspace)).ToList();
307 308 309 310 311 312 313
                return actions.Count == 0
                    ? null
                    : actions.Count == refactoring.Actions.Count
                        ? refactoring
                        : new CodeRefactoring(refactoring.Provider, actions);
            }

314 315 316
            /// <summary>
            /// Arrange fixes into groups based on the issue (diagnostic being fixed) and prioritize these groups.
            /// </summary>
317
            private IEnumerable<SuggestedActionSet> OrganizeFixes(Workspace workspace, IEnumerable<CodeFixCollection> fixCollections, bool hasSuppressionFixes)
318
            {
319 320
                var map = ImmutableDictionary.CreateBuilder<DiagnosticData, IList<SuggestedAction>>();
                var order = ImmutableArray.CreateBuilder<DiagnosticData>();
321 322

                // First group fixes by issue (diagnostic).
323
                GroupFixes(workspace, fixCollections, map, order, hasSuppressionFixes);
324 325 326 327 328 329 330 331

                // Then prioritize between the groups.
                return PrioritizeFixGroups(map.ToImmutable(), order.ToImmutable());
            }

            /// <summary>
            /// Groups fixes by the diagnostic being addressed by each fix.
            /// </summary>
332
            private void GroupFixes(Workspace workspace, IEnumerable<CodeFixCollection> fixCollections, IDictionary<DiagnosticData, IList<SuggestedAction>> map, IList<DiagnosticData> order, bool hasSuppressionFixes)
333 334 335 336 337 338
            {
                foreach (var fixCollection in fixCollections)
                {
                    var fixes = fixCollection.Fixes;
                    var fixCount = fixes.Length;

339 340
                    Func<CodeAction, SuggestedActionSet> getFixAllSuggestedActionSet = codeAction =>
                                CodeFixSuggestedAction.GetFixAllSuggestedActionSet(codeAction, fixCount, fixCollection.FixAllContext,
341
                                    workspace, _subjectBuffer, _owner._editHandler, _owner._waitIndicator);
342

343 344 345 346 347
                    foreach (var fix in fixes)
                    {
                        // Suppression fixes are handled below.
                        if (!(fix.Action is SuppressionCodeAction))
                        {
348 349 350 351 352 353
                            SuggestedAction suggestedAction;
                            if (fix.Action.HasCodeActions)
                            {
                                var nestedActions = new List<SuggestedAction>();
                                foreach (var nestedAction in fix.Action.GetCodeActions())
                                {
354
                                    nestedActions.Add(new CodeFixSuggestedAction(workspace, _subjectBuffer, _owner._editHandler, _owner._waitIndicator,
355 356 357
                                        fix, nestedAction, fixCollection.Provider, getFixAllSuggestedActionSet(nestedAction)));
                                }

358 359
                                var diag = fix.PrimaryDiagnostic;
                                var set = new SuggestedActionSet(nestedActions, SuggestedActionSetPriority.Medium, diag.Location.SourceSpan.ToSpan());
360

361
                                suggestedAction = new SuggestedAction(workspace, _subjectBuffer, _owner._editHandler, _owner._waitIndicator,
362 363 364 365
                                    fix.Action, fixCollection.Provider, new[] { set });
                            }
                            else
                            {
366
                                suggestedAction = new CodeFixSuggestedAction(workspace, _subjectBuffer, _owner._editHandler, _owner._waitIndicator,
367 368
                                    fix, fix.Action, fixCollection.Provider, getFixAllSuggestedActionSet(fix.Action));
                            }
369 370 371 372 373

                            AddFix(fix, suggestedAction, map, order);
                        }
                    }

374
                    if (hasSuppressionFixes)
375
                    {
376 377
                        // Add suppression fixes to the end of a given SuggestedActionSet so that they always show up last in a group.
                        foreach (var fix in fixes)
378
                        {
379 380
                            if (fix.Action is SuppressionCodeAction)
                            {
381 382 383
                                SuggestedAction suggestedAction;
                                if (fix.Action.HasCodeActions)
                                {
384
                                    suggestedAction = new SuppressionSuggestedAction(workspace, _subjectBuffer, _owner._editHandler, _owner._waitIndicator,
385 386 387 388
                                        fix, fixCollection.Provider, getFixAllSuggestedActionSet);
                                }
                                else
                                {
389
                                    suggestedAction = new CodeFixSuggestedAction(workspace, _subjectBuffer, _owner._editHandler, _owner._waitIndicator,
390 391
                                        fix, fix.Action, fixCollection.Provider, getFixAllSuggestedActionSet(fix.Action));
                                }
392

393 394
                                AddFix(fix, suggestedAction, map, order);
                            }
395 396 397 398 399
                        }
                    }
                }
            }

400
            private static void AddFix(CodeFix fix, SuggestedAction suggestedAction, IDictionary<DiagnosticData, IList<SuggestedAction>> map, IList<DiagnosticData> order)
401
            {
402
                var diag = fix.GetPrimaryDiagnosticData();
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
                if (!map.ContainsKey(diag))
                {
                    // Remember the order of the keys for the 'map' dictionary.
                    order.Add(diag);
                    map[diag] = ImmutableArray.CreateBuilder<SuggestedAction>();
                }

                map[diag].Add(suggestedAction);
            }

            /// <summary>
            /// Return prioritized set of fix groups such that fix group for suppression always show up at the bottom of the list.
            /// </summary>
            /// <remarks>
            /// Fix groups are returned in priority order determined based on <see cref="ExtensionOrderAttribute"/>.
            /// Priority for all <see cref="SuggestedActionSet"/>s containing fixes is set to <see cref="SuggestedActionSetPriority.Medium"/> by default.
            /// The only exception is the case where a <see cref="SuggestedActionSet"/> only contains suppression fixes -
            /// the priority of such <see cref="SuggestedActionSet"/>s is set to <see cref="SuggestedActionSetPriority.None"/> so that suppression fixes
            /// always show up last after all other fixes (and refactorings) for the selected line of code.
            /// </remarks>
423
            private static IEnumerable<SuggestedActionSet> PrioritizeFixGroups(IDictionary<DiagnosticData, IList<SuggestedAction>> map, IList<DiagnosticData> order)
424 425 426 427 428 429 430 431 432
            {
                var sets = ImmutableArray.CreateBuilder<SuggestedActionSet>();

                foreach (var diag in order)
                {
                    var fixes = map[diag];

                    var priority = fixes.All(s => s is SuppressionSuggestedAction) ? SuggestedActionSetPriority.None : SuggestedActionSetPriority.Medium;

433 434 435
                    // diagnostic from things like build shouldn't reach here since we don't support LB for those diagnostics
                    Contract.Requires(diag.HasTextSpan);
                    sets.Add(new SuggestedActionSet(fixes, priority, diag.TextSpan.ToSpan()));
436 437 438 439 440 441
                }

                return sets.ToImmutable();
            }

            private IEnumerable<SuggestedActionSet> GetRefactorings(
442
                IDocumentSupportsFeatureService supportsFeatureService,
443 444
                ISuggestedActionCategorySet requestedActionCategories,
                Workspace workspace,
445 446
                Document document,
                SnapshotSpan range,
447 448
                CancellationToken cancellationToken)
            {
449 450
                this.AssertIsForeground();

451
                var optionService = workspace.Services.GetService<IOptionService>();
452

453 454
                if (optionService.GetOption(EditorComponentOnOffOptions.CodeRefactorings) &&
                    _owner._codeRefactoringService != null &&
455
                    supportsFeatureService.SupportsRefactorings(document) &&
456 457
                    requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.Refactoring))
                {
458
                    // Get the selection while on the UI thread.
459
                    var selection = TryGetCodeRefactoringSelection(_subjectBuffer, _textView, range);
460 461 462
                    if (!selection.HasValue)
                    {
                        // this is here to fail test and see why it is failed.
463
                        Trace.WriteLine("given range is not current");
464 465 466 467
                        return null;
                    }

                    var refactorings = Task.Run(
468 469 470 471 472 473
                        async () =>
                        {
                            var stream = await _owner._codeRefactoringService.GetRefactoringsAsync(
                                document, selection.Value, cancellationToken).ConfigureAwait(false);
                            return stream.ToList();
                        },
474
                        cancellationToken).WaitAndGetResult(cancellationToken);
475

476 477 478
                    var filteredRefactorings = FilterOnUIThread(refactorings, workspace);

                    return filteredRefactorings.Select(r => OrganizeRefactorings(workspace, r));
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
                }

                return null;
            }

            /// <summary>
            /// Arrange refactorings into groups.
            /// </summary>
            /// <remarks>
            /// Refactorings are returned in priority order determined based on <see cref="ExtensionOrderAttribute"/>.
            /// Priority for all <see cref="SuggestedActionSet"/>s containing refactorings is set to <see cref="SuggestedActionSetPriority.Low"/>
            /// and should show up after fixes but before suppression fixes in the light bulb menu.
            /// </remarks>
            private SuggestedActionSet OrganizeRefactorings(Workspace workspace, CodeRefactoring refactoring)
            {
                var refactoringSuggestedActions = ImmutableArray.CreateBuilder<SuggestedAction>();

                foreach (var a in refactoring.Actions)
                {
                    refactoringSuggestedActions.Add(
                        new CodeRefactoringSuggestedAction(
500
                            workspace, _subjectBuffer, _owner._editHandler, _owner._waitIndicator, a, refactoring.Provider));
501 502 503 504 505 506 507
                }

                return new SuggestedActionSet(refactoringSuggestedActions.ToImmutable(), SuggestedActionSetPriority.Low);
            }

            public async Task<bool> HasSuggestedActionsAsync(ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, CancellationToken cancellationToken)
            {
508
                // Explicitly hold onto below fields in locals and use these locals throughout this code path to avoid crashes
509
                // if these fields happen to be cleared by Dispose() below. This is required since this code path involves
510
                // code that can run asynchronously from background thread.
511 512 513 514 515 516 517 518 519
                var view = _textView;
                var buffer = _subjectBuffer;
                var provider = _owner;

                if (view == null || buffer == null || provider == null)
                {
                    return false;
                }

C
Charles Stoner 已提交
520
                using (var asyncToken = provider._listener.BeginAsyncOperation("HasSuggestedActionsAsync"))
521
                {
522 523
                    var documentAndSnapshot = await GetMatchingDocumentAndSnapshotAsync(range.Snapshot, cancellationToken).ConfigureAwait(false);
                    if (!documentAndSnapshot.HasValue)
524
                    {
525 526
                        // this is here to fail test and see why it is failed.
                        Trace.WriteLine("given range is not current");
527 528 529
                        return false;
                    }

530 531
                    var document = documentAndSnapshot.Value.Item1;
                    var workspace = document.Project.Solution.Workspace;
532
                    var supportsFeatureService = workspace.Services.GetService<IDocumentSupportsFeatureService>();
533 534 535

                    return
                        await HasFixesAsync(
536
                            supportsFeatureService, requestedActionCategories, provider, document, range,
537 538
                            cancellationToken).ConfigureAwait(false) ||
                        await HasRefactoringsAsync(
539
                            supportsFeatureService, requestedActionCategories, provider, document, buffer, view, range,
540 541 542 543 544
                            cancellationToken).ConfigureAwait(false);
                }
            }

            private async Task<bool> HasFixesAsync(
545
                IDocumentSupportsFeatureService supportsFeatureService,
546 547 548 549 550
                ISuggestedActionCategorySet requestedActionCategories,
                SuggestedActionsSourceProvider provider,
                Document document, SnapshotSpan range,
                CancellationToken cancellationToken)
            {
551
                if (provider._codeFixService != null && supportsFeatureService.SupportsCodeFixes(document) &&
552 553 554 555 556 557 558 559 560 561 562
                    requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.CodeFix))
                {
                    // We only consider suppressions if lightbulb is asking for everything.
                    // If the light bulb is only asking for code fixes, then we don't consider suppressions.
                    var considerSuppressionFixes = requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.Any);
                    var result = await Task.Run(
                        async () => await provider._codeFixService.GetFirstDiagnosticWithFixAsync(
                            document, range.Span.ToTextSpan(), considerSuppressionFixes, cancellationToken).ConfigureAwait(false),
                        cancellationToken).ConfigureAwait(false);

                    if (result.HasFix)
563 564 565 566 567
                    {
                        Logger.Log(FunctionId.SuggestedActions_HasSuggestedActionsAsync);
                        return true;
                    }

568
                    if (result.PartialResult)
569 570 571 572 573 574
                    {
                        // reset solution version number so that we can raise suggested action changed event
                        Volatile.Write(ref _lastSolutionVersionReported, InvalidSolutionVersion);
                        return false;
                    }
                }
575 576

                return false;
577 578
            }

579
            private async Task<bool> HasRefactoringsAsync(
580
                IDocumentSupportsFeatureService supportsFeatureService,
581 582
                ISuggestedActionCategorySet requestedActionCategories,
                SuggestedActionsSourceProvider provider,
583 584 585 586
                Document document,
                ITextBuffer buffer,
                ITextView view,
                SnapshotSpan range,
587
                CancellationToken cancellationToken)
588
            {
589 590 591 592
                var optionService = document.Project.Solution.Workspace.Services.GetService<IOptionService>();

                if (optionService.GetOption(EditorComponentOnOffOptions.CodeRefactorings) &&
                    provider._codeRefactoringService != null &&
593
                    supportsFeatureService.SupportsRefactorings(document) &&
594
                    requestedActionCategories.Contains(PredefinedSuggestedActionCategoryNames.Refactoring))
595
                {
596 597 598 599
                    TextSpan? selection = null;
                    if (IsForeground())
                    {
                        // This operation needs to happen on UI thread because it needs to access textView.Selection.
600
                        selection = TryGetCodeRefactoringSelection(buffer, view, range);
601 602 603 604 605 606
                    }
                    else
                    {
                        await InvokeBelowInputPriority(() =>
                        {
                            // This operation needs to happen on UI thread because it needs to access textView.Selection.
607
                            selection = TryGetCodeRefactoringSelection(buffer, view, range);
608 609
                        }).ConfigureAwait(false);
                    }
610

611 612 613 614 615 616
                    if (!selection.HasValue)
                    {
                        // this is here to fail test and see why it is failed.
                        Trace.WriteLine("given range is not current");
                        return false;
                    }
617

618 619 620 621
                    return await Task.Run(
                        async () => await provider._codeRefactoringService.HasRefactoringsAsync(
                            document, selection.Value, cancellationToken).ConfigureAwait(false),
                        cancellationToken).ConfigureAwait(false);
622 623
                }

624
                return false;
625 626
            }

627
            private static TextSpan? TryGetCodeRefactoringSelection(ITextBuffer buffer, ITextView view, SnapshotSpan range)
628
            {
629 630 631
                var selectedSpans = view.Selection.SelectedSpans
                    .SelectMany(ss => view.BufferGraph.MapDownToBuffer(ss, SpanTrackingMode.EdgeExclusive, buffer))
                    .Where(ss => !view.IsReadOnlyOnSurfaceBuffer(ss))
632 633 634 635 636 637 638 639
                    .ToList();

                // We only support refactorings when there is a single selection in the document.
                if (selectedSpans.Count != 1)
                {
                    return null;
                }

640 641 642 643 644 645 646 647 648
                var translatedSpan = selectedSpans[0].TranslateTo(range.Snapshot, SpanTrackingMode.EdgeInclusive);

                // We only support refactorings when selected span intersects with the span that the light bulb is asking for.
                if (!translatedSpan.IntersectsWith(range))
                {
                    return null;
                }

                return translatedSpan.Span.ToTextSpan();
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696
            }

            private static async Task<ValueTuple<Document, ITextSnapshot>?> GetMatchingDocumentAndSnapshotAsync(ITextSnapshot givenSnapshot, CancellationToken cancellationToken)
            {
                var buffer = givenSnapshot.TextBuffer;
                if (buffer == null)
                {
                    return null;
                }

                var workspace = buffer.GetWorkspace();
                if (workspace == null)
                {
                    return null;
                }

                var documentId = workspace.GetDocumentIdInCurrentContext(buffer.AsTextContainer());
                if (documentId == null)
                {
                    return null;
                }

                var document = workspace.CurrentSolution.GetDocument(documentId);
                if (document == null)
                {
                    return null;
                }

                var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
                cancellationToken.ThrowIfCancellationRequested();

                var snapshot = sourceText.FindCorrespondingEditorTextSnapshot();
                if (snapshot == null || snapshot.Version.ReiteratedVersionNumber != givenSnapshot.Version.ReiteratedVersionNumber)
                {
                    return null;
                }

                return ValueTuple.Create(document, snapshot);
            }

            private void OnTextViewClosed(object sender, EventArgs e)
            {
                Dispose();
            }

            private void OnWorkspaceChanged(object sender, EventArgs e)
            {
                // REVIEW: this event should give both old and new workspace as argument so that
697
                // one doesn't need to hold onto workspace in field.
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733

                // remove existing event registration
                if (_workspace != null)
                {
                    _workspace.DocumentActiveContextChanged -= OnActiveContextChanged;
                }

                // REVIEW: why one need to get new workspace from registration? why not just pass in the new workspace?
                // add new event registration
                _workspace = _registration.Workspace;

                if (_workspace != null)
                {
                    _workspace.DocumentActiveContextChanged += OnActiveContextChanged;
                }
            }

            private void OnActiveContextChanged(object sender, DocumentEventArgs e)
            {
                // REVIEW: it would be nice for changed event to pass in both old and new document.
                OnSuggestedActionsChanged(e.Document.Project.Solution.Workspace, e.Document.Id, e.Document.Project.Solution.WorkspaceVersion);
            }

            private void OnDiagnosticsUpdated(object sender, DiagnosticsUpdatedArgs e)
            {
                // document removed case. no reason to raise event
                if (e.Solution == null)
                {
                    return;
                }

                OnSuggestedActionsChanged(e.Workspace, e.DocumentId, e.Solution.WorkspaceVersion);
            }

            private void OnSuggestedActionsChanged(Workspace currentWorkspace, DocumentId currentDocumentId, int solutionVersion, DiagnosticsUpdatedArgs args = null)
            {
734 735 736 737 738
                // Explicitly hold onto the _subjectBuffer field in a local and use this local in this function to avoid crashes
                // if this field happens to be cleared by Dispose() below. This is required since this code path involves code
                // that can run on background thread.
                var buffer = _subjectBuffer;
                if (buffer == null)
739 740 741 742
                {
                    return;
                }

743
                var workspace = buffer.GetWorkspace();
744 745 746 747 748 749 750

                // workspace is not ready, nothing to do.
                if (workspace == null || workspace != currentWorkspace)
                {
                    return;
                }

751
                if (currentDocumentId != workspace.GetDocumentIdInCurrentContext(buffer.AsTextContainer()) ||
752 753 754 755
                    solutionVersion == Volatile.Read(ref _lastSolutionVersionReported))
                {
                    return;
                }
C
Cyrus Najmabadi 已提交
756
                this.SuggestedActionsChanged?.Invoke(this, EventArgs.Empty);
757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795

                Volatile.Write(ref _lastSolutionVersionReported, solutionVersion);
            }

            public void Dispose()
            {
                if (_owner != null)
                {
                    var updateSource = (IDiagnosticUpdateSource)_owner._diagnosticService;
                    updateSource.DiagnosticsUpdated -= OnDiagnosticsUpdated;
                    _owner = null;
                }

                if (_workspace != null)
                {
                    _workspace.DocumentActiveContextChanged -= OnActiveContextChanged;
                    _workspace = null;
                }

                if (_registration != null)
                {
                    _registration.WorkspaceChanged -= OnWorkspaceChanged;
                    _registration = null;
                }

                if (_textView != null)
                {
                    _textView.Closed -= OnTextViewClosed;
                    _textView = null;
                }

                if (_subjectBuffer != null)
                {
                    _subjectBuffer = null;
                }
            }
        }
    }
}