File: src\Workspaces\SharedUtilitiesAndExtensions\Workspace\CSharp\Indentation\CSharpIndentationService.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.Composition;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.LanguageService;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Indentation;
 
[ExportLanguageService(typeof(IIndentationService), LanguageNames.CSharp), Shared]
internal sealed partial class CSharpIndentationService : AbstractIndentationService<CompilationUnitSyntax>
{
    public static readonly CSharpIndentationService Instance = new();
 
    [ImportingConstructor]
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Incorrectly used in production code: https://github.com/dotnet/roslyn/issues/42839")]
    public CSharpIndentationService()
    {
    }
 
    protected override ISyntaxFacts SyntaxFacts
        => CSharpSyntaxFacts.Instance;
 
    protected override IHeaderFacts HeaderFacts
        => CSharpHeaderFacts.Instance;
 
    protected override ISyntaxFormatting SyntaxFormatting
        => CSharpSyntaxFormatting.Instance;
 
    protected override AbstractFormattingRule GetSpecializedIndentationFormattingRule(FormattingOptions2.IndentStyle indentStyle)
        => CSharpIndentationFormattingRule.Instance;
 
    public static bool ShouldUseSmartTokenFormatterInsteadOfIndenter(
        IEnumerable<AbstractFormattingRule> formattingRules,
        CompilationUnitSyntax root,
        TextLine line,
        IndentationOptions options,
        out SyntaxToken token)
    {
        Contract.ThrowIfNull(formattingRules);
        Contract.ThrowIfNull(root);
 
        token = default;
        if (!options.AutoFormattingOptions.FormatOnReturn)
        {
            return false;
        }
 
        if (options.IndentStyle != FormattingOptions2.IndentStyle.Smart)
        {
            return false;
        }
 
        var firstNonWhitespacePosition = line.GetFirstNonWhitespacePosition();
        if (!firstNonWhitespacePosition.HasValue)
        {
            return false;
        }
 
        token = root.FindToken(firstNonWhitespacePosition.Value);
        if (IsInvalidToken(token))
        {
            return false;
        }
 
        if (token.IsKind(SyntaxKind.None) ||
            token.SpanStart != firstNonWhitespacePosition)
        {
            return false;
        }
 
        // first see whether there is a line operation for current token
        var previousToken = token.GetPreviousToken(includeZeroWidth: true);
 
        // only use smart token formatter when we have two visible tokens.
        if (previousToken.Kind() == SyntaxKind.None || previousToken.IsMissing)
        {
            return false;
        }
 
        var lineOperation = FormattingOperations.GetAdjustNewLinesOperation(formattingRules, previousToken, token, options.FormattingOptions);
        if (lineOperation == null || lineOperation.Option == AdjustNewLinesOption.ForceLinesIfOnSingleLine)
        {
            // no indentation operation, nothing to do for smart token formatter
            return false;
        }
 
        // We're pressing enter between two tokens, have the formatter figure out hte appropriate
        // indentation.
        return true;
    }
 
    private static bool IsInvalidToken(SyntaxToken token)
    {
        // invalid token to be formatted
        return token.Kind() is SyntaxKind.None or SyntaxKind.EndOfDirectiveToken or SyntaxKind.EndOfFileToken;
    }
 
    private class CSharpIndentationFormattingRule : AbstractFormattingRule
    {
        public static readonly AbstractFormattingRule Instance = new CSharpIndentationFormattingRule();
 
        public override void AddIndentBlockOperations(List<IndentBlockOperation> list, SyntaxNode node, in NextIndentBlockOperationAction nextOperation)
        {
            nextOperation.Invoke();
 
            ReplaceCaseIndentationRules(list, node);
 
            if (node is BaseParameterListSyntax ||
                node is TypeArgumentListSyntax ||
                node is TypeParameterListSyntax ||
                node.IsKind(SyntaxKind.Interpolation))
            {
                AddIndentBlockOperations(list, node);
                return;
            }
 
            if (node is BaseArgumentListSyntax argument &&
                !argument.Parent.IsKind(SyntaxKind.ThisConstructorInitializer) &&
                !IsBracketedArgumentListMissingBrackets(argument as BracketedArgumentListSyntax))
            {
                AddIndentBlockOperations(list, argument);
                return;
            }
 
            // only valid if the user has started to actually type a constructor initializer
            if (node is ConstructorInitializerSyntax constructorInitializer &&
                constructorInitializer.ArgumentList.OpenParenToken.Kind() != SyntaxKind.None &&
                !constructorInitializer.ThisOrBaseKeyword.IsMissing)
            {
                var text = node.SyntaxTree.GetText();
 
                // 3 different cases
                // first case : this or base is the first token on line
                // second case : colon is the first token on line
                var colonIsFirstTokenOnLine = !constructorInitializer.ColonToken.IsMissing && constructorInitializer.ColonToken.IsFirstTokenOnLine(text);
                var thisOrBaseIsFirstTokenOnLine = !constructorInitializer.ThisOrBaseKeyword.IsMissing && constructorInitializer.ThisOrBaseKeyword.IsFirstTokenOnLine(text);
 
                if (colonIsFirstTokenOnLine || thisOrBaseIsFirstTokenOnLine)
                {
                    list.Add(FormattingOperations.CreateRelativeIndentBlockOperation(
                        constructorInitializer.ThisOrBaseKeyword,
                        constructorInitializer.ArgumentList.OpenParenToken.GetNextToken(includeZeroWidth: true),
                        constructorInitializer.ArgumentList.CloseParenToken.GetPreviousToken(includeZeroWidth: true),
                        indentationDelta: 1,
                        option: IndentBlockOption.RelativePosition));
                }
                else
                {
                    // third case : none of them are the first token on the line
                    AddIndentBlockOperations(list, constructorInitializer.ArgumentList);
                }
            }
        }
 
        private static bool IsBracketedArgumentListMissingBrackets(BracketedArgumentListSyntax? node)
            => node != null && node.OpenBracketToken.IsMissing && node.CloseBracketToken.IsMissing;
 
        private static void ReplaceCaseIndentationRules(List<IndentBlockOperation> list, SyntaxNode node)
        {
            if (node is not SwitchSectionSyntax section || section.Statements.Count == 0)
            {
                return;
            }
 
            var startToken = section.Statements.First().GetFirstToken(includeZeroWidth: true);
            var endToken = section.Statements.Last().GetLastToken(includeZeroWidth: true);
 
            for (var i = 0; i < list.Count; i++)
            {
                var operation = list[i];
                if (operation.StartToken == startToken && operation.EndToken == endToken)
                {
                    // replace operation
                    list[i] = FormattingOperations.CreateIndentBlockOperation(startToken, endToken, indentationDelta: 1, option: IndentBlockOption.RelativePosition);
                }
            }
        }
 
        private static void AddIndentBlockOperations(List<IndentBlockOperation> list, SyntaxNode node)
        {
            RoslynDebug.AssertNotNull(node.Parent);
 
            // only add indent block operation if the base token is the first token on line
            var baseToken = node.Parent.GetFirstToken(includeZeroWidth: true);
 
            list.Add(FormattingOperations.CreateRelativeIndentBlockOperation(
                baseToken,
                node.GetFirstToken(includeZeroWidth: true).GetNextToken(includeZeroWidth: true),
                node.GetLastToken(includeZeroWidth: true).GetPreviousToken(includeZeroWidth: true),
                indentationDelta: 1,
                option: IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine));
        }
    }
}