File: Wrapping\AbstractCodeActionComputer.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.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Wrapping;
 
internal abstract partial class AbstractSyntaxWrapper
{
    /// <summary>
    /// Class responsible for actually computing the entire set of code actions to offer the
    /// user.  Contains lots of helper functionality used by all the different Wrapper
    /// implementations.
    /// 
    /// Specifically subclasses of this type can simply provide a list of code-actions to
    /// perform.  This type will then take those code actions and will ensure there aren't
    /// multiple code actions that end up having the same effect on the document.  For example,
    /// a "wrap all" action may produce the same results as a "wrap long" action.  In that case
    /// this type will only keep around the first of those actions to prevent showing the user
    /// something that will be unclear.
    /// </summary>
    protected abstract class AbstractCodeActionComputer<TWrapper> : ICodeActionComputer
        where TWrapper : AbstractSyntaxWrapper
    {
        /// <summary>
        /// Annotation used so that we can track the top-most node we want to format after
        /// performing all our edits.
        /// </summary>
        private static readonly SyntaxAnnotation s_toFormatAnnotation = new();
 
        protected readonly TWrapper Wrapper;
 
        protected readonly Document OriginalDocument;
        protected readonly SourceText OriginalSourceText;
        protected readonly SyntaxWrappingOptions Options;
 
        protected readonly SyntaxTriviaList NewLineTrivia;
        protected readonly SyntaxTriviaList SingleWhitespaceTrivia;
        protected readonly SyntaxTriviaList NoTrivia;
 
        /// <summary>
        /// The contents of the documents we've created code-actions for.  This is used so that
        /// we can prevent creating multiple code actions that produce the same results.
        /// </summary>
        private readonly List<SyntaxNode> _seenDocumentRoots = [];
 
        public AbstractCodeActionComputer(
            TWrapper service,
            Document document,
            SourceText originalSourceText,
            SyntaxWrappingOptions options)
        {
            Wrapper = service;
            OriginalDocument = document;
            OriginalSourceText = originalSourceText;
            Options = options;
 
            var generator = SyntaxGenerator.GetGenerator(document);
            var generatorInternal = document.GetRequiredLanguageService<SyntaxGeneratorInternal>();
            NewLineTrivia = new SyntaxTriviaList(generatorInternal.EndOfLine(options.FormattingOptions.NewLine));
            SingleWhitespaceTrivia = new SyntaxTriviaList(generator.Whitespace(" "));
        }
 
        protected abstract Task<ImmutableArray<WrappingGroup>> ComputeWrappingGroupsAsync(CancellationToken cancellationToken);
 
        protected Task<string> GetSmartIndentationAfterAsync(SyntaxNodeOrToken nodeOrToken, CancellationToken cancellationToken)
            => GetIndentationAfterAsync(nodeOrToken, FormattingOptions2.IndentStyle.Smart, cancellationToken);
 
        protected async Task<string> GetIndentationAfterAsync(
            SyntaxNodeOrToken nodeOrToken, FormattingOptions2.IndentStyle indentStyle, CancellationToken cancellationToken)
        {
            var newLine = Options.FormattingOptions.NewLine;
            var newSourceText = OriginalSourceText.WithChanges(
                new TextChange(TextSpan.FromBounds(nodeOrToken.Span.End, OriginalSourceText.Length), newLine));
 
            var newDocument = OriginalDocument.WithText(newSourceText);
 
            // The only auto-formatting option that's relevant is indent style. Others only control behavior on typing.
            var indentationOptions = new IndentationOptions(Options.FormattingOptions) { IndentStyle = indentStyle };
 
            var indentationService = Wrapper.IndentationService;
            var originalLineNumber = newSourceText.Lines.GetLineFromPosition(nodeOrToken.Span.End).LineNumber;
 
            // TODO: should be async https://github.com/dotnet/roslyn/issues/61998
            var newParsedDocument = await ParsedDocument.CreateAsync(newDocument, cancellationToken).ConfigureAwait(false);
 
            var desiredIndentation = indentationService.GetIndentation(
                newParsedDocument, originalLineNumber + 1,
                indentationOptions,
                cancellationToken);
 
            return desiredIndentation.GetIndentationString(newSourceText, Options.FormattingOptions.UseTabs, Options.FormattingOptions.TabSize);
        }
 
        /// <summary>
        /// Try to create a CodeAction representing these edits.  Can return <see langword="null"/> in several 
        /// cases, including:
        /// 
        ///     1. No edits.
        ///     2. Edits would change more than whitespace.
        ///     3. A previous code action was created that already had the same effect.
        /// </summary>
        protected async Task<WrapItemsAction?> TryCreateCodeActionAsync(
            ImmutableArray<Edit> edits, string parentTitle, string title, CancellationToken cancellationToken)
        {
            // First, rewrite the tree with the edits provided.
            var (root, rewrittenRoot, spanToFormat) = await RewriteTreeAsync(edits, cancellationToken).ConfigureAwait(false);
            if (rewrittenRoot == null)
            {
                // Couldn't rewrite for some reason.  No code action to create.
                return null;
            }
 
            // Now, format the part of the tree that we edited.  This will ensure we properly 
            // respect the user preferences around things like comma/operator spacing.
            var formattedDocument = await FormatDocumentAsync(rewrittenRoot, spanToFormat, cancellationToken).ConfigureAwait(false);
            var formattedRoot = await formattedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
            // Now, check if this new formatted tree matches our starting tree, or any of the
            // trees we've already created for our other code actions.  If so, we don't want to
            // add this duplicative code action.  Note: this check will actually run quickly.
            // 'IsEquivalentTo' can return quickly when comparing equivalent green nodes.  So
            // all that we need to check is the spine of the change which will happen very
            // quickly.
 
            if (root.IsEquivalentTo(formattedRoot))
            {
                return null;
            }
 
            foreach (var seenRoot in _seenDocumentRoots)
            {
                if (seenRoot.IsEquivalentTo(formattedRoot))
                {
                    return null;
                }
            }
 
            // This is a genuinely different code action from all previous ones we've created.
            // Store the root so we don't just end up creating this code action again.
            _seenDocumentRoots.Add(formattedRoot);
 
            return new WrapItemsAction(title, parentTitle, (_, _) => Task.FromResult(formattedDocument));
        }
 
        private async Task<Document> FormatDocumentAsync(
            SyntaxNode rewrittenRoot, TextSpan spanToFormat, CancellationToken cancellationToken)
        {
            var newDocument = OriginalDocument.WithSyntaxRoot(rewrittenRoot);
            var formattedDocument = await Formatter.FormatAsync(
                newDocument, spanToFormat, Options.FormattingOptions, cancellationToken).ConfigureAwait(false);
            return formattedDocument;
        }
 
        private async Task<(SyntaxNode root, SyntaxNode rewrittenRoot, TextSpan spanToFormat)> RewriteTreeAsync(
            ImmutableArray<Edit> edits, CancellationToken cancellationToken)
        {
            using var _1 = PooledDictionary<SyntaxToken, SyntaxTriviaList>.GetInstance(out var leftTokenToTrailingTrivia);
            using var _2 = PooledDictionary<SyntaxToken, SyntaxTriviaList>.GetInstance(out var rightTokenToLeadingTrivia);
 
            foreach (var edit in edits)
            {
                var span = TextSpan.FromBounds(edit.Left.Span.End, edit.Right.Span.Start);
                var text = OriginalSourceText.ToString(span);
                if (!IsSafeToRemove(text))
                {
                    // editing some piece of non-whitespace trivia.  We don't support this.
                    return default;
                }
 
                // Make sure we're not about to make an edit that just changes the code to what
                // is already there.
                if (text != edit.GetNewTrivia())
                {
                    leftTokenToTrailingTrivia.Add(edit.Left, edit.NewLeftTrailingTrivia);
                    rightTokenToLeadingTrivia.Add(edit.Right, edit.NewRightLeadingTrivia);
                }
            }
 
            if (leftTokenToTrailingTrivia.Count == 0)
            {
                // No actual edits that would change anything.  Nothing to do.
                return default;
            }
 
            return await RewriteTreeAsync(
                leftTokenToTrailingTrivia, rightTokenToLeadingTrivia, cancellationToken).ConfigureAwait(false);
        }
 
        private static bool IsSafeToRemove(string text)
        {
            foreach (var ch in text)
            {
                // It's safe to remove whitespace between tokens, or the VB line-continuation character.
                if (!char.IsWhiteSpace(ch) && ch != '_')
                {
                    return false;
                }
            }
 
            return true;
        }
 
        private async Task<(SyntaxNode root, SyntaxNode rewrittenRoot, TextSpan spanToFormat)> RewriteTreeAsync(
            Dictionary<SyntaxToken, SyntaxTriviaList> leftTokenToTrailingTrivia,
            Dictionary<SyntaxToken, SyntaxTriviaList> rightTokenToLeadingTrivia,
            CancellationToken cancellationToken)
        {
            var root = await OriginalDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var tokens = leftTokenToTrailingTrivia.Keys.Concat(rightTokenToLeadingTrivia.Keys).Distinct().ToImmutableArray();
 
            // Find the closest node that contains all the tokens we're editing.  That's the
            // node we'll format at the end.  This will ensure that all formattin respects
            // user settings for things like spacing around commas/operators/etc.
            var nodeToFormat = tokens.SelectAsArray(t => t.Parent).FindInnermostCommonNode<SyntaxNode>();
            Contract.ThrowIfNull(nodeToFormat);
 
            // Rewrite the tree performing the following actions:
            //
            //  1. Add an annotation to nodeToFormat so that we can find that node again after
            //     updating all tokens.
            //
            //  2. Hit all tokens in the two passed in maps, and update their leading/trailing
            //     trivia accordingly.
 
            var rewrittenRoot = root.ReplaceSyntax(
                nodes: [nodeToFormat],
                computeReplacementNode: (oldNode, newNode) => newNode.WithAdditionalAnnotations(s_toFormatAnnotation),
 
                tokens: leftTokenToTrailingTrivia.Keys.Concat(rightTokenToLeadingTrivia.Keys).Distinct(),
                computeReplacementToken: (oldToken, newToken) =>
                {
                    if (leftTokenToTrailingTrivia.TryGetValue(oldToken, out var trailingTrivia))
                    {
                        newToken = newToken.WithTrailingTrivia(trailingTrivia);
                    }
 
                    if (rightTokenToLeadingTrivia.TryGetValue(oldToken, out var leadingTrivia))
                    {
                        newToken = newToken.WithLeadingTrivia(leadingTrivia);
                    }
 
                    return newToken;
                },
                trivia: null,
                computeReplacementTrivia: null);
 
            Contract.ThrowIfNull(rewrittenRoot);
            var trackedNode = rewrittenRoot.GetAnnotatedNodes(s_toFormatAnnotation).Single();
 
            return (root, rewrittenRoot, trackedNode.Span);
        }
 
        public async Task<ImmutableArray<CodeAction>> GetTopLevelCodeActionsAsync(CancellationToken cancellationToken)
        {
            try
            {
                // Ask subclass to produce whole nested list of wrapping code actions
                var wrappingGroups = await ComputeWrappingGroupsAsync(cancellationToken).ConfigureAwait(false);
 
                using var result = TemporaryArray<CodeAction>.Empty;
                foreach (var group in wrappingGroups)
                {
                    // if a group is empty just ignore it.
                    var wrappingActions = group.WrappingActions.WhereNotNull().ToImmutableArray();
                    if (wrappingActions.Length == 0)
                    {
                        continue;
                    }
 
                    // If a group only has one item, and subclass says the item is inlinable,
                    // then just directly return that nested item as a top level item.
                    if (wrappingActions.Length == 1 && group.IsInlinable)
                    {
                        result.Add(wrappingActions[0]);
                        continue;
                    }
 
                    // Otherwise, sort items and add to the resultant list
                    var sorted = WrapItemsAction.SortActionsByMostRecentlyUsed(ImmutableArray<CodeAction>.CastUp(wrappingActions));
 
                    // 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.
                    result.Add(CodeAction.Create(
                        wrappingActions[0].ParentTitle, sorted,
                        group.IsInlinable, CodeActionPriority.Low));
                }
 
                // Finally, sort the topmost list we're building and return that.  This ensures that
                // both the top level items and the nested items are ordered appropriate.
                return WrapItemsAction.SortActionsByMostRecentlyUsed(result.ToImmutableAndClear());
            }
            catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex, cancellationToken, ErrorSeverity.Diagnostic))
            {
                throw;
            }
        }
    }
}