UnifiedSuggestedActionsSource.cs 32.5 KB
Newer Older
A
Allison Chou 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.CodeActions.CodeAction;
using CodeFixGroupKey = System.Tuple<Microsoft.CodeAnalysis.Diagnostics.DiagnosticData, Microsoft.CodeAnalysis.CodeActions.CodeActionPriority, Microsoft.CodeAnalysis.CodeActions.CodeActionPriority?>;

namespace Microsoft.CodeAnalysis.UnifiedSuggestions
{
    /// <summary>
    /// Provides mutual code action logic for both local and LSP scenarios
    /// via intermediate interface <see cref="IUnifiedSuggestedAction"/>.
    /// </summary>
    internal class UnifiedSuggestedActionsSource
    {
        /// <summary>
32
        /// Gets, filters, and orders code fixes.
A
Allison Chou 已提交
33
        /// </summary>
34
        public static async Task<ImmutableArray<UnifiedSuggestedActionSet>> GetFilterAndOrderCodeFixesAsync(
A
Allison Chou 已提交
35 36 37 38 39 40 41 42 43 44 45 46 47
            Workspace workspace,
            ICodeFixService codeFixService,
            Document document,
            TextSpan selection,
            bool includeSuppressionFixes,
            bool isBlocking,
            Func<string, IDisposable?> addOperationScope,
            CancellationToken cancellationToken)
        {
            // It may seem strange that we kick off a task, but then immediately 'Wait' on 
            // it. However, it's deliberate.  We want to make sure that the code runs on 
            // the background so that no one takes an accidentally dependency on running on 
            // the UI thread.
48
            var fixes = await Task.Run(
A
Allison Chou 已提交
49 50
                () => codeFixService.GetFixesAsync(
                document, selection, includeSuppressionFixes, isBlocking,
51
                addOperationScope, cancellationToken), cancellationToken).ConfigureAwait(false);
A
Allison Chou 已提交
52

53
            var filteredFixes = FilterOnAnyThread(fixes);
A
Allison Chou 已提交
54 55 56 57
            var organizedFixes = OrganizeFixes(workspace, filteredFixes, includeSuppressionFixes);

            return organizedFixes;
        }
A
Allison Chou 已提交
58

59
        private static ImmutableArray<CodeFixCollection> FilterOnAnyThread(ImmutableArray<CodeFixCollection> collections)
A
Allison Chou 已提交
60
        {
61
            return collections.Select(c => FilterIndividuallyOnAnyThread(c)).WhereNotNull().ToImmutableArray();
A
Allison Chou 已提交
62 63
        }

64
        private static CodeFixCollection? FilterIndividuallyOnAnyThread(CodeFixCollection collection)
A
Allison Chou 已提交
65
        {
66
            var applicableFixes = collection.Fixes;
A
Allison Chou 已提交
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
            return applicableFixes.Length == 0
                ? null
                : applicableFixes.Length == collection.Fixes.Length
                    ? collection
                    : new CodeFixCollection(
                        collection.Provider, collection.TextSpan, applicableFixes,
                        collection.FixAllState, collection.SupportedScopes, collection.FirstDiagnostic);
        }

        /// <summary>
        /// Arrange fixes into groups based on the issue (diagnostic being fixed) and prioritize these groups.
        /// </summary>
        private static ImmutableArray<UnifiedSuggestedActionSet> OrganizeFixes(
            Workspace workspace,
            ImmutableArray<CodeFixCollection> fixCollections,
            bool includeSuppressionFixes)
        {
            var map = ImmutableDictionary.CreateBuilder<CodeFixGroupKey, IList<UnifiedSuggestedAction>>();
A
Allison Chou 已提交
85
            using var _ = ArrayBuilder<CodeFixGroupKey>.GetInstance(out var order);
A
Allison Chou 已提交
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

            // First group fixes by diagnostic and priority.
            GroupFixes(workspace, fixCollections, map, order, includeSuppressionFixes);

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

        /// <summary>
        /// Groups fixes by the diagnostic being addressed by each fix.
        /// </summary>
        private static void GroupFixes(
           Workspace workspace,
           ImmutableArray<CodeFixCollection> fixCollections,
           IDictionary<CodeFixGroupKey, IList<UnifiedSuggestedAction>> map,
           ArrayBuilder<CodeFixGroupKey> order,
           bool includeSuppressionFixes)
        {
            foreach (var fixCollection in fixCollections)
            {
                ProcessFixCollection(
                    workspace, map, order, includeSuppressionFixes, fixCollection);
            }
        }

        private static void ProcessFixCollection(
            Workspace workspace,
            IDictionary<CodeFixGroupKey, IList<UnifiedSuggestedAction>> map,
            ArrayBuilder<CodeFixGroupKey> order,
            bool includeSuppressionFixes,
            CodeFixCollection fixCollection)
        {
            var fixes = fixCollection.Fixes;
            var fixCount = fixes.Length;

            var nonSupressionCodeFixes = fixes.WhereAsArray(f => !IsTopLevelSuppressionAction(f.Action));
            var supressionCodeFixes = fixes.WhereAsArray(f => IsTopLevelSuppressionAction(f.Action));

            AddCodeActions(workspace, map, order, fixCollection,
A
Allison Chou 已提交
126
                GetFixAllSuggestedActionSet, nonSupressionCodeFixes);
A
Allison Chou 已提交
127 128 129 130 131 132

            // Add suppression fixes to the end of a given SuggestedActionSet so that they
            // always show up last in a group.
            if (includeSuppressionFixes)
            {
                AddCodeActions(workspace, map, order, fixCollection,
A
Allison Chou 已提交
133
                    GetFixAllSuggestedActionSet, supressionCodeFixes);
A
Allison Chou 已提交
134
            }
A
Allison Chou 已提交
135 136 137 138 139 140 141 142 143

            return;

            // Local functions
            UnifiedSuggestedActionSet? GetFixAllSuggestedActionSet(CodeAction codeAction)
                => GetUnifiedFixAllSuggestedActionSet(
                    codeAction, fixCount, fixCollection.FixAllState,
                    fixCollection.SupportedScopes, fixCollection.FirstDiagnostic,
                    workspace);
A
Allison Chou 已提交
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
        }

        private static void AddCodeActions(
            Workspace workspace, IDictionary<CodeFixGroupKey, IList<UnifiedSuggestedAction>> map,
            ArrayBuilder<CodeFixGroupKey> order, CodeFixCollection fixCollection,
            Func<CodeAction, UnifiedSuggestedActionSet?> getFixAllSuggestedActionSet,
            ImmutableArray<CodeFix> codeFixes)
        {
            foreach (var fix in codeFixes)
            {
                var unifiedSuggestedAction = GetUnifiedSuggestedAction(fix.Action, fix);
                AddFix(fix, unifiedSuggestedAction, map, order);
            }

            return;

            // Local functions
            UnifiedSuggestedAction GetUnifiedSuggestedAction(CodeAction action, CodeFix fix)
            {
                if (action.NestedCodeActions.Length > 0)
                {
                    var nestedActions = action.NestedCodeActions.SelectAsArray(
                        nestedAction => GetUnifiedSuggestedAction(nestedAction, fix));

A
Allison Chou 已提交
168 169
                    var set = new UnifiedSuggestedActionSet(
                        categoryName: null,
A
Allison Chou 已提交
170
                        actions: nestedActions,
A
Allison Chou 已提交
171
                        title: null,
A
Allison Chou 已提交
172 173 174 175
                        priority: GetUnifiedSuggestedActionSetPriority(action.Priority),
                        applicableToSpan: fix.PrimaryDiagnostic.Location.SourceSpan);

                    return new UnifiedSuggestedActionWithNestedActions(
A
Allison Chou 已提交
176
                        workspace, action, action.Priority, fixCollection.Provider, ImmutableArray.Create(set));
A
Allison Chou 已提交
177 178 179 180
                }
                else
                {
                    return new UnifiedCodeFixSuggestedAction(
A
Allison Chou 已提交
181 182
                        workspace, action, action.Priority, fix, fixCollection.Provider,
                        getFixAllSuggestedActionSet(action));
A
Allison Chou 已提交
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
                }
            }
        }

        private static void AddFix(
            CodeFix fix, UnifiedSuggestedAction suggestedAction,
            IDictionary<CodeFixGroupKey, IList<UnifiedSuggestedAction>> map,
            ArrayBuilder<CodeFixGroupKey> order)
        {
            var groupKey = GetGroupKey(fix);
            if (!map.ContainsKey(groupKey))
            {
                order.Add(groupKey);
                map[groupKey] = ImmutableArray.CreateBuilder<UnifiedSuggestedAction>();
            }

            map[groupKey].Add(suggestedAction);
A
Allison Chou 已提交
200
            return;
A
Allison Chou 已提交
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242

            static CodeFixGroupKey GetGroupKey(CodeFix fix)
            {
                var diag = fix.GetPrimaryDiagnosticData();
                if (fix.Action is AbstractConfigurationActionWithNestedActions configurationAction)
                {
                    return new CodeFixGroupKey(
                        diag, configurationAction.Priority, configurationAction.AdditionalPriority);
                }

                return new CodeFixGroupKey(diag, fix.Action.Priority, null);
            }
        }

        // If the provided fix all context is non-null and the context's code action Id matches
        // the given code action's Id, returns the set of fix all occurrences actions associated
        // with the code action.
        private static UnifiedSuggestedActionSet? GetUnifiedFixAllSuggestedActionSet(
            CodeAction action,
            int actionCount,
            FixAllState fixAllState,
            ImmutableArray<FixAllScope> supportedScopes,
            Diagnostic firstDiagnostic,
            Workspace workspace)
        {

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

            if (actionCount > 1 && action.EquivalenceKey == null)
            {
                return null;
            }

            using var fixAllSuggestedActionsDisposer = ArrayBuilder<UnifiedFixAllSuggestedAction>.GetInstance(
                out var fixAllSuggestedActions);
            foreach (var scope in supportedScopes)
            {
                var fixAllStateForScope = fixAllState.WithScopeAndEquivalenceKey(scope, action.EquivalenceKey);
                var fixAllSuggestedAction = new UnifiedFixAllSuggestedAction(
A
Allison Chou 已提交
243
                    workspace, action, action.Priority, fixAllStateForScope, firstDiagnostic);
A
Allison Chou 已提交
244 245 246 247 248 249 250

                fixAllSuggestedActions.Add(fixAllSuggestedAction);
            }

            return new UnifiedSuggestedActionSet(
                categoryName: null,
                actions: fixAllSuggestedActions.ToImmutable(),
A
Allison Chou 已提交
251 252 253
                title: CodeFixesResources.Fix_all_occurrences_in,
                priority: UnifiedSuggestedActionSetPriority.None,
                applicableToSpan: null);
A
Allison Chou 已提交
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
        }

        /// <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="UnifiedSuggestedActionSet"/>s containing fixes is set to
        /// <see cref="UnifiedSuggestedActionSetPriority.Medium"/> by default.
        /// The only exception is the case where a <see cref="UnifiedSuggestedActionSet"/> only contains suppression fixes -
        /// the priority of such <see cref="UnifiedSuggestedActionSet"/>s is set to
        /// <see cref="UnifiedSuggestedActionSetPriority.None"/> so that suppression fixes
        /// always show up last after all other fixes (and refactorings) for the selected line of code.
        /// </remarks>
        private static ImmutableArray<UnifiedSuggestedActionSet> PrioritizeFixGroups(
            ImmutableDictionary<CodeFixGroupKey, IList<UnifiedSuggestedAction>> map,
            ImmutableArray<CodeFixGroupKey> order,
            Workspace workspace)
        {
A
Allison Chou 已提交
273 274 275
            using var _1 = ArrayBuilder<UnifiedSuggestedActionSet>.GetInstance(out var nonSuppressionSets);
            using var _2 = ArrayBuilder<UnifiedSuggestedActionSet>.GetInstance(out var suppressionSets);
            using var _3 = ArrayBuilder<UnifiedSuggestedAction>.GetInstance(out var bulkConfigurationActions);
A
Allison Chou 已提交
276 277 278 279 280

            foreach (var groupKey in order)
            {
                var actions = map[groupKey];

281
                var nonSuppressionActions = actions.Where(a => !IsTopLevelSuppressionAction(a.OriginalCodeAction));
A
Allison Chou 已提交
282 283
                AddUnifiedSuggestedActionsSet(nonSuppressionActions, groupKey, nonSuppressionSets);

284 285
                var suppressionActions = actions.Where(a => IsTopLevelSuppressionAction(a.OriginalCodeAction) &&
                    !IsBulkConfigurationAction(a.OriginalCodeAction));
A
Allison Chou 已提交
286 287
                AddUnifiedSuggestedActionsSet(suppressionActions, groupKey, suppressionSets);

288
                bulkConfigurationActions.AddRange(actions.Where(a => IsBulkConfigurationAction(a.OriginalCodeAction)));
A
Allison Chou 已提交
289 290 291 292 293 294 295 296
            }

            var sets = nonSuppressionSets.ToImmutable();

            // Append bulk configuration fixes at the end of suppression/configuration fixes.
            if (bulkConfigurationActions.Count > 0)
            {
                var bulkConfigurationSet = new UnifiedSuggestedActionSet(
A
Allison Chou 已提交
297 298
                    UnifiedPredefinedSuggestedActionCategoryNames.CodeFix, bulkConfigurationActions.ToArray(),
                    title: null, priority: UnifiedSuggestedActionSetPriority.None, applicableToSpan: null);
A
Allison Chou 已提交
299 300 301 302 303 304 305
                suppressionSets.Add(bulkConfigurationSet);
            }

            if (suppressionSets.Count > 0)
            {
                // Wrap the suppression/configuration actions within another top level suggested action
                // to avoid clutter in the light bulb menu.
A
Allison Chou 已提交
306
                var suppressOrConfigureCodeAction = new NoChangeAction(CodeFixesResources.Suppress_or_Configure_issues);
A
Allison Chou 已提交
307
                var wrappingSuggestedAction = new UnifiedSuggestedActionWithNestedActions(
A
Allison Chou 已提交
308 309
                    workspace, codeAction: suppressOrConfigureCodeAction,
                    codeActionPriority: suppressOrConfigureCodeAction.Priority, provider: null,
A
Allison Chou 已提交
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 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 370 371 372 373
                    nestedActionSets: suppressionSets.ToImmutable());

                // Combine the spans and the category of each of the nested suggested actions
                // to get the span and category for the new top level suggested action.
                var (span, category) = CombineSpansAndCategory(suppressionSets);
                var wrappingSet = new UnifiedSuggestedActionSet(
                    category,
                    actions: SpecializedCollections.SingletonEnumerable(wrappingSuggestedAction),
                    title: CodeFixesResources.Suppress_or_Configure_issues,
                    priority: UnifiedSuggestedActionSetPriority.None,
                    applicableToSpan: span);
                sets = sets.Add(wrappingSet);
            }

            return sets;

            // Local functions
            static (TextSpan? span, string category) CombineSpansAndCategory(IEnumerable<UnifiedSuggestedActionSet> sets)
            {
                // We are combining the spans and categories of the given set of suggested action sets
                // to generate a result span containing the spans of individual suggested action sets and
                // a result category which is the maximum severity category amongst the set
                var minStart = -1;
                var maxEnd = -1;
                var category = UnifiedPredefinedSuggestedActionCategoryNames.CodeFix;

                foreach (var set in sets)
                {
                    if (set.ApplicableToSpan.HasValue)
                    {
                        var currentStart = set.ApplicableToSpan.Value.Start;
                        var currentEnd = set.ApplicableToSpan.Value.End;

                        if (minStart == -1 || currentStart < minStart)
                        {
                            minStart = currentStart;
                        }

                        if (maxEnd == -1 || currentEnd > maxEnd)
                        {
                            maxEnd = currentEnd;
                        }
                    }

                    Debug.Assert(set.CategoryName == UnifiedPredefinedSuggestedActionCategoryNames.CodeFix ||
                                 set.CategoryName == UnifiedPredefinedSuggestedActionCategoryNames.ErrorFix);

                    // If this set contains an error fix, then change the result category to ErrorFix
                    if (set.CategoryName == UnifiedPredefinedSuggestedActionCategoryNames.ErrorFix)
                    {
                        category = UnifiedPredefinedSuggestedActionCategoryNames.ErrorFix;
                    }
                }

                var combinedSpan = minStart >= 0 ? new TextSpan(minStart, maxEnd) : (TextSpan?)null;
                return (combinedSpan, category);
            }
        }

        private static void AddUnifiedSuggestedActionsSet(
            IEnumerable<UnifiedSuggestedAction> actions,
            CodeFixGroupKey groupKey,
            ArrayBuilder<UnifiedSuggestedActionSet> sets)
        {
A
Allison Chou 已提交
374
            foreach (var group in actions.GroupBy(a => a.CodeActionPriority))
A
Allison Chou 已提交
375 376 377 378 379 380 381
            {
                var priority = GetUnifiedSuggestedActionSetPriority(group.Key);

                // diagnostic from things like build shouldn't reach here since we don't support LB for those diagnostics
                Debug.Assert(groupKey.Item1.HasTextSpan);
                var category = GetFixCategory(groupKey.Item1.Severity);
                sets.Add(new UnifiedSuggestedActionSet(
A
Allison Chou 已提交
382
                    category, group, title: null, priority: priority, applicableToSpan: groupKey.Item1.GetTextSpan()));
A
Allison Chou 已提交
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
            }
        }

        private static string GetFixCategory(DiagnosticSeverity severity)
        {
            switch (severity)
            {
                case DiagnosticSeverity.Hidden:
                case DiagnosticSeverity.Info:
                case DiagnosticSeverity.Warning:
                    return UnifiedPredefinedSuggestedActionCategoryNames.CodeFix;
                case DiagnosticSeverity.Error:
                    return UnifiedPredefinedSuggestedActionCategoryNames.ErrorFix;
                default:
                    throw ExceptionUtilities.Unreachable;
            }
        }

        private static bool IsTopLevelSuppressionAction(CodeAction action)
            => action is AbstractConfigurationActionWithNestedActions;

        private static bool IsBulkConfigurationAction(CodeAction action)
            => (action as AbstractConfigurationActionWithNestedActions)?.IsBulkConfigurationAction == true;

        /// <summary>
408
        /// Gets, filters, and orders code refactorings.
A
Allison Chou 已提交
409
        /// </summary>
410
        public static async Task<ImmutableArray<UnifiedSuggestedActionSet>> GetFilterAndOrderCodeRefactoringsAsync(
A
Allison Chou 已提交
411 412 413 414 415 416 417 418 419 420 421 422 423
            Workspace workspace,
            ICodeRefactoringService codeRefactoringService,
            Document document,
            TextSpan selection,
            bool isBlocking,
            Func<string, IDisposable?> addOperationScope,
            bool filterOutsideSelection,
            CancellationToken cancellationToken)
        {
            // It may seem strange that we kick off a task, but then immediately 'Wait' on 
            // it. However, it's deliberate.  We want to make sure that the code runs on 
            // the background so that no one takes an accidentally dependency on running on 
            // the UI thread.
424
            var refactorings = await Task.Run(
A
Allison Chou 已提交
425 426
                () => codeRefactoringService.GetRefactoringsAsync(
                    document, selection, isBlocking, addOperationScope,
427
                    cancellationToken), cancellationToken).ConfigureAwait(false);
A
Allison Chou 已提交
428

429
            var filteredRefactorings = FilterOnAnyThread(refactorings, selection, filterOutsideSelection);
A
Allison Chou 已提交
430 431 432 433 434

            return filteredRefactorings.SelectAsArray(
                r => OrganizeRefactorings(workspace, r));
        }

435
        private static ImmutableArray<CodeRefactoring> FilterOnAnyThread(
A
Allison Chou 已提交
436 437
            ImmutableArray<CodeRefactoring> refactorings,
            TextSpan selection,
438 439
            bool filterOutsideSelection)
            => refactorings.Select(r => FilterOnAnyThread(r, selection, filterOutsideSelection)).WhereNotNull().ToImmutableArray();
A
Allison Chou 已提交
440

441
        private static CodeRefactoring? FilterOnAnyThread(
A
Allison Chou 已提交
442 443
            CodeRefactoring refactoring,
            TextSpan selection,
444
            bool filterOutsideSelection)
A
Allison Chou 已提交
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
        {
            var actions = refactoring.CodeActions.WhereAsArray(IsActionAndSpanApplicable);
            return actions.Length == 0
                ? null
                : actions.Length == refactoring.CodeActions.Length
                    ? refactoring
                    : new CodeRefactoring(refactoring.Provider, actions);

            bool IsActionAndSpanApplicable((CodeAction action, TextSpan? applicableSpan) actionAndSpan)
            {
                if (filterOutsideSelection)
                {
                    // Filter out refactorings with applicable span outside the selection span.
                    if (!actionAndSpan.applicableSpan.HasValue ||
                        !selection.IntersectsWith(actionAndSpan.applicableSpan.Value))
                    {
                        return false;
                    }
                }

                return true;
            }
        }

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

            foreach (var codeAction in refactoring.CodeActions)
            {
                if (codeAction.action.NestedCodeActions.Length > 0)
                {
                    var nestedActions = codeAction.action.NestedCodeActions.SelectAsArray(
                        na => new UnifiedCodeRefactoringSuggestedAction(
A
Allison Chou 已提交
489
                            workspace, na, na.Priority, refactoring.Provider));
A
Allison Chou 已提交
490 491 492 493

                    var set = new UnifiedSuggestedActionSet(
                        categoryName: null,
                        actions: nestedActions,
A
Allison Chou 已提交
494
                        title: null,
A
Allison Chou 已提交
495 496 497 498
                        priority: GetUnifiedSuggestedActionSetPriority(codeAction.action.Priority),
                        applicableToSpan: codeAction.applicableToSpan);

                    refactoringSuggestedActions.Add(
A
Allison Chou 已提交
499 500
                        new UnifiedSuggestedActionWithNestedActions(
                            workspace, codeAction.action, codeAction.action.Priority, refactoring.Provider, ImmutableArray.Create(set)));
A
Allison Chou 已提交
501 502 503 504
                }
                else
                {
                    refactoringSuggestedActions.Add(
A
Allison Chou 已提交
505 506
                        new UnifiedCodeRefactoringSuggestedAction(
                            workspace, codeAction.action, codeAction.action.Priority, refactoring.Provider));
A
Allison Chou 已提交
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
                }
            }

            var actions = refactoringSuggestedActions.ToImmutable();

            // An action set:
            // - gets the the same priority as the highest priority action within in.
            // - gets `applicableToSpan` of the first action:
            //   - E.g. the `applicableToSpan` closest to current selection might be a more correct 
            //     choice. All actions created by one Refactoring have usually the same `applicableSpan`
            //     and therefore the complexity of determining the closest one isn't worth the benefit
            //     of slightly more correct orderings in certain edge cases.
            return new UnifiedSuggestedActionSet(
                UnifiedPredefinedSuggestedActionCategoryNames.Refactoring,
                actions: actions,
A
Allison Chou 已提交
522 523
                title: null,
                priority: GetUnifiedSuggestedActionSetPriority(actions.Max(a => a.CodeActionPriority)),
A
Allison Chou 已提交
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
                applicableToSpan: refactoring.CodeActions.FirstOrDefault().applicableToSpan);
        }

        private static UnifiedSuggestedActionSetPriority GetUnifiedSuggestedActionSetPriority(CodeActionPriority key)
            => key switch
            {
                CodeActionPriority.None => UnifiedSuggestedActionSetPriority.None,
                CodeActionPriority.Low => UnifiedSuggestedActionSetPriority.Low,
                CodeActionPriority.Medium => UnifiedSuggestedActionSetPriority.Medium,
                CodeActionPriority.High => UnifiedSuggestedActionSetPriority.High,
                _ => throw new InvalidOperationException(),
            };

        /// <summary>
        /// Filters and orders the code fix sets and code refactoring sets amongst each other.
539 540
        /// Should be called with the results from <see cref="GetFilterAndOrderCodeFixesAsync"/>
        /// and <see cref="GetFilterAndOrderCodeRefactoringsAsync"/>.
A
Allison Chou 已提交
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
        /// </summary>
        public static ImmutableArray<UnifiedSuggestedActionSet>? FilterAndOrderActionSets(
            ImmutableArray<UnifiedSuggestedActionSet> fixes,
            ImmutableArray<UnifiedSuggestedActionSet> refactorings,
            TextSpan? selectionOpt)
        {
            // Get the initial set of action sets, with refactorings and fixes appropriately
            // ordered against each other.
            var result = GetInitiallyOrderedActionSets(selectionOpt, fixes, refactorings);
            if (result.IsEmpty)
            {
                return null;
            }

            // Now that we have the entire set of action sets, inline, sort and filter
            // them appropriately against each other.
            var allActionSets = InlineActionSetsIfDesirable(result);
            var orderedActionSets = OrderActionSets(allActionSets, selectionOpt);
            var filteredSets = FilterActionSetsByTitle(orderedActionSets);

            return filteredSets;
        }

        private static ImmutableArray<UnifiedSuggestedActionSet> GetInitiallyOrderedActionSets(
            TextSpan? selectionOpt,
            ImmutableArray<UnifiedSuggestedActionSet> fixes,
            ImmutableArray<UnifiedSuggestedActionSet> refactorings)
        {
            // First, order refactorings based on the order the providers actually gave for
            // their actions. This way, a low pri refactoring always shows after a medium pri
            // refactoring, no matter what we do below.
            refactorings = OrderActionSets(refactorings, selectionOpt);

            // If there's a selection, it's likely the user is trying to perform some operation
            // directly on that operation (like 'extract method').  Prioritize refactorings over
            // fixes in that case.  Otherwise, it's likely that the user is just on some error
            // and wants to fix it (in which case, prioritize fixes).

            if (selectionOpt?.Length > 0)
            {
                // There was a selection.  Treat refactorings as more important than fixes.
                // Note: we still will sort after this.  So any high pri fixes will come to the
                // front.  Any low-pri refactorings will go to the end.
                return refactorings.Concat(fixes);
            }
            else
            {
                // No selection.  Treat all medium and low pri refactorings as low priority, and
                // place after fixes.  Even a low pri fixes will be above what was *originally*
                // a medium pri refactoring.
                //
                // Note: we do not do this for *high* pri refactorings (like 'rename').  These
                // are still very important and need to stay at the top (though still after high
                // pri fixes).
                var highPriRefactorings = refactorings.WhereAsArray(
                    s => s.Priority == UnifiedSuggestedActionSetPriority.High);
                var nonHighPriRefactorings = refactorings.WhereAsArray(
                    s => s.Priority != UnifiedSuggestedActionSetPriority.High)
                        .SelectAsArray(s => WithPriority(s, UnifiedSuggestedActionSetPriority.Low));

                var highPriFixes = fixes.WhereAsArray(s => s.Priority == UnifiedSuggestedActionSetPriority.High);
                var nonHighPriFixes = fixes.WhereAsArray(s => s.Priority != UnifiedSuggestedActionSetPriority.High);

                return highPriFixes.Concat(highPriRefactorings).Concat(nonHighPriFixes).Concat(nonHighPriRefactorings);
            }
        }

        private static ImmutableArray<UnifiedSuggestedActionSet> OrderActionSets(
            ImmutableArray<UnifiedSuggestedActionSet> actionSets, TextSpan? selectionOpt)
        {
            return actionSets.OrderByDescending(s => s.Priority)
                             .ThenBy(s => s, new UnifiedSuggestedActionSetComparer(selectionOpt))
                             .ToImmutableArray();
        }

        private static UnifiedSuggestedActionSet WithPriority(
            UnifiedSuggestedActionSet set, UnifiedSuggestedActionSetPriority priority)
            => new UnifiedSuggestedActionSet(set.CategoryName, set.Actions, set.Title, priority, set.ApplicableToSpan);

        private static ImmutableArray<UnifiedSuggestedActionSet> InlineActionSetsIfDesirable(
            ImmutableArray<UnifiedSuggestedActionSet> 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.SelectAsArray(InlineActions);
        }

        private static UnifiedSuggestedActionSet InlineActions(UnifiedSuggestedActionSet actionSet)
        {
            using var newActionsDisposer = ArrayBuilder<IUnifiedSuggestedAction>.GetInstance(out var newActions);
            foreach (var action in actionSet.Actions)
            {
                var actionWithNestedActions = action as UnifiedSuggestedActionWithNestedActions;

                // Only inline if the underlying code action allows it.
642
                if (actionWithNestedActions?.OriginalCodeAction.IsInlinable == true)
A
Allison Chou 已提交
643 644 645 646 647 648 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
                {
                    newActions.AddRange(actionWithNestedActions.NestedActionSets.SelectMany(set => set.Actions));
                }
                else
                {
                    newActions.Add(action);
                }
            }

            return new UnifiedSuggestedActionSet(
                actionSet.CategoryName,
                newActions.ToImmutable(),
                actionSet.Title,
                actionSet.Priority,
                actionSet.ApplicableToSpan);
        }

        private static ImmutableArray<UnifiedSuggestedActionSet> FilterActionSetsByTitle(
            ImmutableArray<UnifiedSuggestedActionSet> allActionSets)
        {
            using var resultDisposer = ArrayBuilder<UnifiedSuggestedActionSet>.GetInstance(out var result);
            var seenTitles = new HashSet<string>();

            foreach (var set in allActionSets)
            {
                var filteredSet = FilterActionSetByTitle(set, seenTitles);
                if (filteredSet != null)
                {
                    result.Add(filteredSet);
                }
            }

            return result.ToImmutable();
        }

        private static UnifiedSuggestedActionSet? FilterActionSetByTitle(UnifiedSuggestedActionSet set, HashSet<string> seenTitles)
        {
            using var actionsDisposer = ArrayBuilder<IUnifiedSuggestedAction>.GetInstance(out var actions);

            foreach (var action in set.Actions)
            {
684
                if (seenTitles.Add(action.OriginalCodeAction.Title))
A
Allison Chou 已提交
685 686 687 688 689 690 691 692 693 694 695
                {
                    actions.Add(action);
                }
            }

            return actions.Count == 0
                ? null
                : new UnifiedSuggestedActionSet(set.CategoryName, actions.ToImmutable(), set.Title, set.Priority, set.ApplicableToSpan);
        }
    }
}