File: Wrapping\SeparatedSyntaxList\SeparatedSyntaxListCodeActionComputer.cs
Web Access
Project: src\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.
 
using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Wrapping.SeparatedSyntaxList;
 
internal abstract partial class AbstractSeparatedSyntaxListWrapper<TListSyntax, TListItemSyntax>
{
    /// <summary>
    /// Class responsible for actually computing the entire set of code actions to offer the user.
    /// </summary>
    private sealed class SeparatedSyntaxListCodeActionComputer : AbstractCodeActionComputer<AbstractSeparatedSyntaxListWrapper<TListSyntax, TListItemSyntax>>
    {
        private readonly TListSyntax _listSyntax;
        private readonly SeparatedSyntaxList<TListItemSyntax> _listItems;
 
        /// <summary>
        /// The indentation string necessary to indent an item in a list such that the start of
        /// that item will exact start at the end of the open-token for the containing list. i.e.
        /// 
        ///     void Goobar(
        ///                 ^
        ///                 |
        /// 
        /// This is the indentation we want when we're aligning wrapped items with the first item 
        /// in the list.
        /// </summary>
        private readonly SyntaxTrivia _afterOpenTokenIndentationTrivia;
 
        /// <summary>
        /// Indentation amount for any items that have been wrapped to a new line.  Valid if we're
        /// not aligning with the first item. i.e.
        /// 
        ///     void Goobar(
        ///         ^
        ///         |
        /// </summary>
        private readonly AsyncLazy<SyntaxTrivia> _singleIndentationTrivia;
 
        /// <summary>
        /// Indentation to use when placing brace.  e.g.:
        /// 
        ///     var v = new List {
        ///     ^
        ///     |
        /// </summary>
        private readonly AsyncLazy<SyntaxTrivia> _braceIndentationTrivia;
 
        /// <summary>
        /// Whether or not we should move the open brace of this separated list to a new line.  Many separated lists
        /// will never move the brace (like a parameter list).  And some separated lists may move the brace
        /// depending on if a particular option is set (like the collection initializer brace in C#).
        /// </summary>
        private readonly bool _shouldMoveOpenBraceToNewLine;
 
        /// <summary>
        /// Whether or not we should move the close brace of this separated list to a new line.  Some lists will
        /// never move the close brace (like a parameter list), while some will always move it (like a collection
        /// initializer in both C# or VB).
        /// </summary>
        private readonly bool _shouldMoveCloseBraceToNewLine;
 
        public SeparatedSyntaxListCodeActionComputer(
            AbstractSeparatedSyntaxListWrapper<TListSyntax, TListItemSyntax> service,
            Document document,
            SourceText sourceText,
            SyntaxWrappingOptions options,
            TListSyntax listSyntax,
            SeparatedSyntaxList<TListItemSyntax> listItems)
            : base(service, document, sourceText, options)
        {
            _listSyntax = listSyntax;
            _listItems = listItems;
 
            _shouldMoveOpenBraceToNewLine = service.ShouldMoveOpenBraceToNewLine(options);
            _shouldMoveCloseBraceToNewLine = service.ShouldMoveCloseBraceToNewLine;
 
            var generator = SyntaxGenerator.GetGenerator(OriginalDocument);
 
            _afterOpenTokenIndentationTrivia = generator.Whitespace(GetAfterOpenTokenIndentation());
            _singleIndentationTrivia = AsyncLazy.Create(async cancellationToken => generator.Whitespace(await GetSingleIndentationAsync(cancellationToken).ConfigureAwait(false)));
            _braceIndentationTrivia = AsyncLazy.Create(async cancellationToken => generator.Whitespace(await GetBraceTokenIndentationAsync(cancellationToken).ConfigureAwait(false)));
        }
 
        private async Task AddTextChangeBetweenOpenAndFirstItemAsync(
            WrappingStyle wrappingStyle, ArrayBuilder<Edit> result, CancellationToken cancellationToken)
        {
            result.Add(wrappingStyle == WrappingStyle.WrapFirst_IndentRest
                ? Edit.UpdateBetween(_listSyntax.GetFirstToken(), NewLineTrivia, await _singleIndentationTrivia.GetValueAsync(cancellationToken).ConfigureAwait(false), _listItems[0])
                : Edit.DeleteBetween(_listSyntax.GetFirstToken(), _listItems[0]));
        }
 
        private string GetAfterOpenTokenIndentation()
        {
            var openToken = _listSyntax.GetFirstToken();
            var afterOpenTokenOffset = OriginalSourceText.GetOffset(openToken.Span.End);
 
            var indentString = afterOpenTokenOffset.CreateIndentationString(Options.FormattingOptions.UseTabs, Options.FormattingOptions.TabSize);
            return indentString;
        }
 
        private Task<string> GetSingleIndentationAsync(CancellationToken cancellationToken)
        {
            // Insert a newline after the open token of the list.  Then ask the
            // ISynchronousIndentationService where it thinks that the next line should be
            // indented.
            var openToken = _listSyntax.GetFirstToken();
 
            return GetSmartIndentationAfterAsync(openToken, cancellationToken);
        }
 
        private async Task<SyntaxTrivia> GetIndentationTriviaAsync(WrappingStyle wrappingStyle, CancellationToken cancellationToken)
        {
            return wrappingStyle == WrappingStyle.UnwrapFirst_AlignRest
                ? _afterOpenTokenIndentationTrivia
                : await _singleIndentationTrivia.GetValueAsync(cancellationToken).ConfigureAwait(false);
        }
 
        private Task<string> GetBraceTokenIndentationAsync(CancellationToken cancellationToken)
        {
            var previousToken = _listSyntax.GetFirstToken().GetPreviousToken();
 
            // Block indentation is the only style that correctly indents across all initializer expressions
            return GetIndentationAfterAsync(previousToken, FormattingOptions2.IndentStyle.Block, cancellationToken);
        }
 
        protected override async Task<ImmutableArray<WrappingGroup>> ComputeWrappingGroupsAsync(CancellationToken cancellationToken)
        {
            using var _ = ArrayBuilder<WrappingGroup>.GetInstance(out var result);
            await AddWrappingGroupsAsync(result, cancellationToken).ConfigureAwait(false);
            return result.ToImmutableAndClear();
        }
 
        private async Task AddWrappingGroupsAsync(
            ArrayBuilder<WrappingGroup> result, CancellationToken cancellationToken)
        {
            result.Add(await GetWrapEveryGroupAsync(cancellationToken).ConfigureAwait(false));
            result.Add(await GetUnwrapGroupAsync(cancellationToken).ConfigureAwait(false));
            result.Add(await GetWrapLongGroupAsync(cancellationToken).ConfigureAwait(false));
        }
 
        #region unwrap group
 
        private async Task<WrappingGroup> GetUnwrapGroupAsync(CancellationToken cancellationToken)
        {
            using var _ = ArrayBuilder<WrapItemsAction>.GetInstance(out var unwrapActions);
 
            var parentTitle = Wrapper.Unwrap_list;
 
            // Unwrap entirely.
            // MethodName(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j)
            unwrapActions.AddIfNotNull(await GetUnwrapAllCodeActionAsync(
                parentTitle, WrappingStyle.UnwrapFirst_IndentRest, cancellationToken).ConfigureAwait(false));
 
            if (this.Wrapper.Supports_UnwrapGroup_WrapFirst_IndentRest)
            {
                // MethodName(
                //      int a, int b, int c, int d, int e, int f, int g, int h, int i, int j)
                //
                // Unwrap the items, adjusting the braces as well.
                unwrapActions.AddIfNotNull(await GetUnwrapAllCodeActionAsync(
                    parentTitle, WrappingStyle.WrapFirst_IndentRest, cancellationToken).ConfigureAwait(false));
            }
 
            // {
            //     int a, int b, int c, int d, int e, int f, int g, int h, int i, int j
            // }
            //
            // without adjusting the braces.
            var unwrapWithoutBraces = await GetWrapLongLineCodeActionAsync(
                parentTitle, WrappingStyle.WrapFirst_IndentRest, wrappingColumn: int.MaxValue, cancellationToken).ConfigureAwait(false);
            unwrapActions.AddIfNotNull(unwrapWithoutBraces);
 
            // The first two unwrap options share no title with anything else (so they can be inlined).  However,
            // the last action shared a title with the wrap-long action.  so we don't inline in that case.
            var isInlinable = unwrapWithoutBraces is null;
 
            return new WrappingGroup(isInlinable, unwrapActions.ToImmutableAndClear());
        }
 
        private async Task<WrapItemsAction?> GetUnwrapAllCodeActionAsync(
            string parentTitle, WrappingStyle wrappingStyle, CancellationToken cancellationToken)
        {
            var edits = await GetUnwrapAllEditsAsync(wrappingStyle, cancellationToken).ConfigureAwait(false);
            var title = wrappingStyle == WrappingStyle.WrapFirst_IndentRest
                ? Wrapper.Unwrap_and_indent_all_items
                : Wrapper.Unwrap_all_items;
 
            return await TryCreateCodeActionAsync(edits, parentTitle, title, cancellationToken).ConfigureAwait(false);
        }
 
        private async Task<ImmutableArray<Edit>> GetUnwrapAllEditsAsync(WrappingStyle wrappingStyle, CancellationToken cancellationToken)
        {
            using var _ = ArrayBuilder<Edit>.GetInstance(out var result);
 
            if (_shouldMoveOpenBraceToNewLine)
                result.Add(Edit.DeleteBetween(_listSyntax.GetFirstToken().GetPreviousToken(), _listSyntax.GetFirstToken()));
 
            await AddTextChangeBetweenOpenAndFirstItemAsync(
                wrappingStyle, result, cancellationToken).ConfigureAwait(false);
 
            foreach (var comma in _listItems.GetSeparators())
            {
                result.Add(Edit.DeleteBetween(comma.GetPreviousToken(), comma));
                result.Add(Edit.DeleteBetween(comma, comma.GetNextToken()));
            }
 
            var last = _listItems.GetWithSeparators().Last();
            if (last.IsNode)
                result.Add(Edit.DeleteBetween(last, _listSyntax.GetLastToken()));
 
            return result.ToImmutableAndClear();
        }
 
        #endregion
 
        #region wrap long line
 
        private async Task<WrappingGroup> GetWrapLongGroupAsync(CancellationToken cancellationToken)
        {
            var parentTitle = Wrapper.Wrap_long_list;
            using var _ = ArrayBuilder<WrapItemsAction>.GetInstance(out var codeActions);
 
            var wrappingColumn = Options.WrappingColumn;
 
            if (this.Wrapper.Supports_WrapLongGroup_UnwrapFirst)
            {
                // MethodName(int a, int b, int c,
                //            int d, int e, int f,
                //            int g, int h, int i,
                //            int j)
                codeActions.AddIfNotNull(await GetWrapLongLineCodeActionAsync(
                    parentTitle, WrappingStyle.UnwrapFirst_AlignRest, wrappingColumn, cancellationToken).ConfigureAwait(false));
            }
 
            // MethodName(
            //     int a, int b, int c, int d, int e,
            //     int f, int g, int h, int i, int j)
            codeActions.AddIfNotNull(await GetWrapLongLineCodeActionAsync(
                parentTitle, WrappingStyle.WrapFirst_IndentRest, wrappingColumn, cancellationToken).ConfigureAwait(false));
 
            if (this.Wrapper.Supports_WrapLongGroup_UnwrapFirst)
            {
                // MethodName(int a, int b, int c, 
                //     int d, int e, int f, int g,
                //     int h, int i, int j)
                codeActions.AddIfNotNull(await GetWrapLongLineCodeActionAsync(
                parentTitle, WrappingStyle.UnwrapFirst_IndentRest, wrappingColumn, cancellationToken).ConfigureAwait(false));
            }
 
            // The wrap-all and wrap-long code action titles are not unique.  i.e. we show them
            // as:
            //      Wrap every parameter:
            //          Align parameters
            //          Indent wrapped parameters
            //      Wrap long parameter list:
            //          Align parameters
            //          Indent wrapped parameters
            //
            // We can't in-line these nested actions because the parent title is necessary to
            // determine which situation each child action applies to.
 
            return new WrappingGroup(isInlinable: false, codeActions.ToImmutable());
        }
 
        private async Task<WrapItemsAction?> GetWrapLongLineCodeActionAsync(
            string parentTitle, WrappingStyle wrappingStyle, int wrappingColumn, CancellationToken cancellationToken)
        {
            var indentationTrivia = await GetIndentationTriviaAsync(wrappingStyle, cancellationToken).ConfigureAwait(false);
 
            var edits = await GetWrapLongLinesEditsAsync(
                wrappingStyle, indentationTrivia, wrappingColumn, cancellationToken).ConfigureAwait(false);
            var title = GetNestedCodeActionTitle(wrappingStyle);
 
            return await TryCreateCodeActionAsync(edits, parentTitle, title, cancellationToken).ConfigureAwait(false);
        }
 
        private async Task<ImmutableArray<Edit>> GetWrapLongLinesEditsAsync(
            WrappingStyle wrappingStyle, SyntaxTrivia indentationTrivia, int wrappingColumn, CancellationToken cancellationToken)
        {
            using var _ = ArrayBuilder<Edit>.GetInstance(out var result);
 
            if (_shouldMoveOpenBraceToNewLine)
            {
                result.Add(Edit.UpdateBetween(
                    _listSyntax.GetFirstToken().GetPreviousToken(), NewLineTrivia,
                    await _braceIndentationTrivia.GetValueAsync(cancellationToken).ConfigureAwait(false),
                    _listSyntax.GetFirstToken()));
            }
 
            await AddTextChangeBetweenOpenAndFirstItemAsync(
                wrappingStyle, result, cancellationToken).ConfigureAwait(false);
 
            var currentOffset = wrappingStyle == WrappingStyle.WrapFirst_IndentRest
                ? indentationTrivia.FullWidth()
                : _afterOpenTokenIndentationTrivia.FullWidth();
            var itemsAndSeparators = _listItems.GetWithSeparators();
 
            for (var i = 0; i < itemsAndSeparators.Count; i += 2)
            {
                var item = itemsAndSeparators[i].AsNode()!;
 
                // Figure out where we'd be after this item.
                currentOffset += item.Span.Length;
 
                if (i > 0)
                {
                    if (currentOffset < wrappingColumn)
                    {
                        // this item would not make us go pass our preferred wrapping column. So
                        // keep it on this line, making sure there's a space between the previous
                        // comma and us.
                        result.Add(Edit.UpdateBetween(itemsAndSeparators[i - 1], SingleWhitespaceTrivia, NoTrivia, item));
                        currentOffset += " ".Length;
                    }
                    else
                    {
                        // not the first item on the line and this item makes us go past the wrapping
                        // limit.  We want to wrap before this item.
                        result.Add(Edit.UpdateBetween(itemsAndSeparators[i - 1], NewLineTrivia, indentationTrivia, item));
                        currentOffset = indentationTrivia.FullWidth() + item.Span.Length;
                    }
                }
 
                // Get rid of any spaces between the list item and the following comma token
                if (i + 1 < itemsAndSeparators.Count)
                {
                    var comma = itemsAndSeparators[i + 1];
                    Contract.ThrowIfFalse(comma.IsToken);
                    result.Add(Edit.DeleteBetween(item, comma));
                    currentOffset += comma.Span.Length;
                }
            }
 
            if (this.Wrapper.ShouldMoveCloseBraceToNewLine)
            {
                result.Add(Edit.UpdateBetween(
                    itemsAndSeparators.Last(), NewLineTrivia,
                    await _braceIndentationTrivia.GetValueAsync(cancellationToken).ConfigureAwait(false),
                    _listSyntax.GetLastToken()));
            }
            else
            {
                result.Add(Edit.DeleteBetween(itemsAndSeparators.Last(), _listSyntax.GetLastToken()));
            }
 
            return result.ToImmutableAndClear();
        }
 
        #endregion
 
        #region wrap every
 
        private async Task<WrappingGroup> GetWrapEveryGroupAsync(CancellationToken cancellationToken)
        {
            var parentTitle = Wrapper.Wrap_every_item;
 
            using var _ = ArrayBuilder<WrapItemsAction>.GetInstance(out var codeActions);
 
            if (this.Wrapper.Supports_WrapEveryGroup_UnwrapFirst)
            {
                // MethodName(int a,
                //            int b,
                //            ...
                //            int j);
                codeActions.AddIfNotNull(await GetWrapEveryNestedCodeActionAsync(
                    parentTitle, WrappingStyle.UnwrapFirst_AlignRest, cancellationToken).ConfigureAwait(false));
            }
 
            // MethodName(
            //     int a,
            //     int b,
            //     ...
            //     int j)
            codeActions.AddIfNotNull(await GetWrapEveryNestedCodeActionAsync(
                parentTitle, WrappingStyle.WrapFirst_IndentRest, cancellationToken).ConfigureAwait(false));
 
            if (this.Wrapper.Supports_WrapEveryGroup_UnwrapFirst)
            {
                // MethodName(int a,
                //     int b,
                //     ...
                //     int j)
                codeActions.AddIfNotNull(await GetWrapEveryNestedCodeActionAsync(
                    parentTitle, WrappingStyle.UnwrapFirst_IndentRest, cancellationToken).ConfigureAwait(false));
            }
 
            // See comment in GetWrapLongTopLevelCodeActionAsync for explanation of why we're
            // not inlinable.
            return new WrappingGroup(isInlinable: false, codeActions.ToImmutable());
        }
 
        private async Task<WrapItemsAction?> GetWrapEveryNestedCodeActionAsync(
            string parentTitle, WrappingStyle wrappingStyle, CancellationToken cancellationToken)
        {
            var indentationTrivia = await GetIndentationTriviaAsync(wrappingStyle, cancellationToken).ConfigureAwait(false);
 
            var edits = await GetWrapEachEditsAsync(wrappingStyle, indentationTrivia, cancellationToken).ConfigureAwait(false);
            var title = GetNestedCodeActionTitle(wrappingStyle);
 
            return await TryCreateCodeActionAsync(edits, parentTitle, title, cancellationToken).ConfigureAwait(false);
        }
 
        private string GetNestedCodeActionTitle(WrappingStyle wrappingStyle)
            => wrappingStyle switch
            {
                WrappingStyle.WrapFirst_IndentRest => Wrapper.Indent_all_items,
                WrappingStyle.UnwrapFirst_AlignRest => Wrapper.Align_wrapped_items,
                WrappingStyle.UnwrapFirst_IndentRest => Wrapper.Indent_wrapped_items,
                _ => throw ExceptionUtilities.UnexpectedValue(wrappingStyle),
            };
 
        private async Task<ImmutableArray<Edit>> GetWrapEachEditsAsync(
            WrappingStyle wrappingStyle, SyntaxTrivia indentationTrivia, CancellationToken cancellationToken)
        {
            using var _ = ArrayBuilder<Edit>.GetInstance(out var result);
 
            if (_shouldMoveOpenBraceToNewLine)
            {
                result.Add(Edit.UpdateBetween(
                    _listSyntax.GetFirstToken().GetPreviousToken(), NewLineTrivia,
                    await _braceIndentationTrivia.GetValueAsync(cancellationToken).ConfigureAwait(false),
                    _listSyntax.GetFirstToken()));
            }
 
            await AddTextChangeBetweenOpenAndFirstItemAsync(wrappingStyle, result, cancellationToken).ConfigureAwait(false);
 
            var itemsAndSeparators = _listItems.GetWithSeparators();
 
            for (var i = 1; i < itemsAndSeparators.Count; i += 2)
            {
                var comma = itemsAndSeparators[i].AsToken();
 
                var item = itemsAndSeparators[i - 1];
                result.Add(Edit.DeleteBetween(item, comma));
 
                if (i < itemsAndSeparators.Count - 1)
                {
                    // Always wrap between this comma and the next item.
                    result.Add(Edit.UpdateBetween(
                        comma, NewLineTrivia, indentationTrivia, itemsAndSeparators[i + 1]));
                }
            }
 
            if (_shouldMoveCloseBraceToNewLine)
            {
                result.Add(Edit.UpdateBetween(
                    itemsAndSeparators.Last(), NewLineTrivia,
                    await _braceIndentationTrivia.GetValueAsync(cancellationToken).ConfigureAwait(false),
                    _listSyntax.GetLastToken()));
            }
            else
            {
                // last item.  Delete whatever is between it and the close token of the list.
                result.Add(Edit.DeleteBetween(itemsAndSeparators.Last(), _listSyntax.GetLastToken()));
            }
 
            return result.ToImmutableAndClear();
        }
 
        #endregion
    }
}