File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\Indentation\CSharpSmartTokenFormatter.cs
Web Access
Project: src\src\Workspaces\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Workspaces.csproj (Microsoft.CodeAnalysis.CSharp.Workspaces)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Indentation;
 
internal class CSharpSmartTokenFormatter : ISmartTokenFormatter
{
    private readonly IndentationOptions _options;
    private readonly ImmutableArray<AbstractFormattingRule> _formattingRules;
 
    private readonly CompilationUnitSyntax _root;
    private readonly SourceText _text;
 
    public CSharpSmartTokenFormatter(
        IndentationOptions options,
        ImmutableArray<AbstractFormattingRule> formattingRules,
        CompilationUnitSyntax root,
        SourceText text)
    {
        Contract.ThrowIfNull(root);
 
        _options = options;
        _formattingRules = formattingRules;
 
        _root = root;
        _text = text;
    }
 
    public IList<TextChange> FormatRange(
        SyntaxToken startToken, SyntaxToken endToken, CancellationToken cancellationToken)
    {
        Contract.ThrowIfTrue(startToken.Kind() is SyntaxKind.None or SyntaxKind.EndOfFileToken);
        Contract.ThrowIfTrue(endToken.Kind() is SyntaxKind.None or SyntaxKind.EndOfFileToken);
 
        var smartTokenformattingRules = _formattingRules;
        var common = startToken.GetCommonRoot(endToken);
        RoslynDebug.AssertNotNull(common);
 
        // if there are errors, do not touch lines
        // Exception 1: In the case of try-catch-finally block, a try block without a catch/finally block is considered incomplete
        //            but we would like to apply line operation in a completed try block even if there is no catch/finally block
        // Exception 2: Similar behavior for do-while
        if (common.ContainsDiagnostics && !CloseBraceOfTryOrDoBlock(endToken))
        {
            smartTokenformattingRules = [new NoLineChangeFormattingRule(), .. _formattingRules];
        }
 
        var formatter = CSharpSyntaxFormatting.Instance;
        var result = formatter.GetFormattingResult(
            _root, [TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End)], _options.FormattingOptions, smartTokenformattingRules, cancellationToken);
        return result.GetTextChanges(cancellationToken);
    }
 
    private static bool CloseBraceOfTryOrDoBlock(SyntaxToken endToken)
    {
        return endToken.IsKind(SyntaxKind.CloseBraceToken) &&
            endToken.Parent.IsKind(SyntaxKind.Block) &&
            endToken.Parent.Parent?.Kind() is SyntaxKind.TryStatement or SyntaxKind.DoStatement;
    }
 
    public IList<TextChange> FormatToken(SyntaxToken token, CancellationToken cancellationToken)
    {
        Contract.ThrowIfTrue(token.Kind() is SyntaxKind.None or SyntaxKind.EndOfFileToken);
 
        // get previous token
        var previousToken = token.GetPreviousToken(includeZeroWidth: true);
        if (previousToken.Kind() == SyntaxKind.None)
        {
            // no previous token. nothing to format
            return [];
        }
 
        // This is a heuristic to prevent brace completion from breaking user expectation/muscle memory in common scenarios (see Devdiv:823958).
        // Formatter uses FindToken on the position, which returns token to left, if there is nothing to the right and returns token to the right
        // if there exists one. If the shape is "{|}", we're including '}' in the formatting range. Avoid doing that to improve verbatim typing
        // in the following special scenarios.  
        var adjustedEndPosition = token.Span.End;
        if (token.IsKind(SyntaxKind.OpenBraceToken) &&
            (token.Parent.IsInitializerForArrayOrCollectionCreationExpression() ||
                token.Parent is AnonymousObjectCreationExpressionSyntax))
        {
            var nextToken = token.GetNextToken(includeZeroWidth: true);
            if (nextToken.IsKind(SyntaxKind.CloseBraceToken))
            {
                // Format upto '{' and exclude '}'
                adjustedEndPosition = token.SpanStart;
            }
        }
 
        ImmutableArray<AbstractFormattingRule> smartTokenFormattingRules = [new SmartTokenFormattingRule(), .. _formattingRules];
        var adjustedStartPosition = previousToken.SpanStart;
        if (token.IsKind(SyntaxKind.OpenBraceToken) &&
            _options.IndentStyle != FormattingOptions2.IndentStyle.Smart)
        {
            RoslynDebug.AssertNotNull(token.SyntaxTree);
            if (token.IsFirstTokenOnLine(_text))
            {
                adjustedStartPosition = token.SpanStart;
            }
        }
 
        var formatter = CSharpSyntaxFormatting.Instance;
        var result = formatter.GetFormattingResult(
            _root, [TextSpan.FromBounds(adjustedStartPosition, adjustedEndPosition)], _options.FormattingOptions, smartTokenFormattingRules, cancellationToken);
        return result.GetTextChanges(cancellationToken);
    }
 
    private class NoLineChangeFormattingRule : AbstractFormattingRule
    {
        public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation)
        {
            // no line operation. no line changes what so ever
            var lineOperation = base.GetAdjustNewLinesOperation(in previousToken, in currentToken, in nextOperation);
            if (lineOperation != null)
            {
                // ignore force if same line option
                if (lineOperation.Option == AdjustNewLinesOption.ForceLinesIfOnSingleLine)
                {
                    return null;
                }
 
                // basically means don't ever put new line if there isn't already one, but do
                // indentation.
                return FormattingOperations.CreateAdjustNewLinesOperation(line: 0, option: AdjustNewLinesOption.PreserveLines);
            }
 
            return null;
        }
    }
 
    private class SmartTokenFormattingRule : NoLineChangeFormattingRule
    {
        public override void AddSuppressOperations(ArrayBuilder<SuppressOperation> list, SyntaxNode node, in NextSuppressOperationAction nextOperation)
        {
            // don't suppress anything
        }
 
        public override AdjustSpacesOperation? GetAdjustSpacesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustSpacesOperation nextOperation)
        {
            var spaceOperation = base.GetAdjustSpacesOperation(in previousToken, in currentToken, in nextOperation);
 
            // if there is force space operation, convert it to ForceSpaceIfSingleLine operation.
            // (force space basically means remove all line breaks)
            if (spaceOperation != null && spaceOperation.Option == AdjustSpacesOption.ForceSpaces)
            {
                return FormattingOperations.CreateAdjustSpacesOperation(spaceOperation.Space, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
 
            return spaceOperation;
        }
    }
}