File: Wrapping\WrapItemsAction.cs
Web Access
Project: ..\..\..\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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 disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using static Microsoft.CodeAnalysis.CodeActions.CodeAction;
 
namespace Microsoft.CodeAnalysis.Wrapping
{
    /// <summary>
    /// Code action for actually wrapping items.  Provided as a special subclass because it will
    /// also update the wrapping most-recently-used list when the code action is actually
    /// invoked.
    /// </summary>
    internal class WrapItemsAction : DocumentChangeAction
    {
        // Keeps track of the invoked code actions.  That way we can prioritize those code actions 
        // in the future since they're more likely the ones the user wants.  This is important as 
        // we have 9 different code actions offered (3 major groups, with 3 actions per group).  
        // It's likely the user will just pick from a few of these. So we'd like the ones they
        // choose to be prioritized accordingly.
        private static ImmutableArray<string> s_mruTitles = ImmutableArray<string>.Empty;
 
        public string ParentTitle { get; }
 
        public string SortTitle { get; }
 
        // Make our code action low priority.  This option will be offered *a lot*, and 
        // much of  the time will not be something the user particularly wants to do.  
        // It should be offered after all other normal refactorings.
        //
        // This value is only relevant if this code action is the only one in its group,
        // and it ends up getting inlined as a top-level-action that is offered.
        public WrapItemsAction(string title, string parentTitle, Func<CancellationToken, Task<Document>> createChangedDocument)
            : base(title, createChangedDocument, title, CodeActionPriority.Low)
        {
            ParentTitle = parentTitle;
            SortTitle = parentTitle + "_" + title;
        }
 
        protected override Task<IEnumerable<CodeActionOperation>> ComputePreviewOperationsAsync(CancellationToken cancellationToken)
        {
            // For preview, we don't want to compute the normal operations.  Specifically, we don't
            // want to compute the stateful operation that tracks which code action was triggered.
            return base.ComputeOperationsAsync(cancellationToken);
        }
 
        protected override async Task<IEnumerable<CodeActionOperation>> ComputeOperationsAsync(CancellationToken cancellationToken)
        {
            var operations = await base.ComputeOperationsAsync(cancellationToken).ConfigureAwait(false);
            var operationsList = operations.ToList();
 
            operationsList.Add(new RecordCodeActionOperation(SortTitle, ParentTitle));
            return operationsList;
        }
 
        public static ImmutableArray<CodeAction> SortActionsByMostRecentlyUsed(ImmutableArray<CodeAction> codeActions)
            => SortByMostRecentlyUsed(codeActions, s_mruTitles, GetSortTitle);
 
        public static ImmutableArray<T> SortByMostRecentlyUsed<T>(
            ImmutableArray<T> items, ImmutableArray<string> mostRecentlyUsedKeys, Func<T, string> getKey)
        {
            return items.Sort((d1, d2) =>
            {
                var mruIndex1 = mostRecentlyUsedKeys.IndexOf(getKey(d1));
                var mruIndex2 = mostRecentlyUsedKeys.IndexOf(getKey(d2));
 
                // If both are in the mru, prefer the one earlier on.
                if (mruIndex1 >= 0 && mruIndex2 >= 0)
                    return mruIndex1 - mruIndex2;
 
                // if either is in the mru, and the other is not, then the mru item is preferred.
                if (mruIndex1 >= 0)
                    return -1;
 
                if (mruIndex2 >= 0)
                    return 1;
 
                // Neither are in the mru.  Sort them based on their original locations.
                var index1 = items.IndexOf(d1);
                var index2 = items.IndexOf(d2);
 
                // Note: we don't return 0 here as ImmutableArray.Sort is not stable.
                return index1 - index2;
            });
        }
 
        private static string GetSortTitle(CodeAction codeAction)
            => (codeAction as WrapItemsAction)?.SortTitle ?? codeAction.Title;
 
        private class RecordCodeActionOperation : CodeActionOperation
        {
            private readonly string _sortTitle;
            private readonly string _parentTitle;
 
            public RecordCodeActionOperation(string sortTitle, string parentTitle)
            {
                _sortTitle = sortTitle;
                _parentTitle = parentTitle;
            }
 
            internal override bool ApplyDuringTests => false;
 
            public override void Apply(Workspace workspace, CancellationToken cancellationToken)
            {
                // Record both the sortTitle of the nested action and the tile of the parent
                // action.  This way we any invocation of a code action helps prioritize both
                // the parent lists and the nested lists.
                s_mruTitles = s_mruTitles.Remove(_sortTitle).Remove(_parentTitle)
                                         .Insert(0, _sortTitle).Insert(0, _parentTitle);
            }
        }
    }
}