File: Formatting\CSharpSyntaxFormattingService.cs
Web Access
Project: ..\..\..\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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Indentation;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Formatting
{
    internal sealed class CSharpSyntaxFormattingService : CSharpSyntaxFormatting, ISyntaxFormattingService
    {
        private readonly LanguageServices _services;
 
        [ExportLanguageServiceFactory(typeof(ISyntaxFormattingService), LanguageNames.CSharp), Shared]
        internal sealed class Factory : ILanguageServiceFactory
        {
            [ImportingConstructor]
            [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
            public Factory()
            {
            }
 
            public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
                => new CSharpSyntaxFormattingService(languageServices.LanguageServices);
        }
 
        private CSharpSyntaxFormattingService(LanguageServices languageServices)
            => _services = languageServices;
 
        public bool ShouldFormatOnTypedCharacter(
            ParsedDocument documentSyntax,
            char typedChar,
            int caretPosition,
            CancellationToken cancellationToken)
        {
            // first, find the token user just typed.
            var token = documentSyntax.Root.FindToken(Math.Max(0, caretPosition - 1), findInsideTrivia: true);
            if (token.IsMissing ||
                !ValidSingleOrMultiCharactersTokenKind(typedChar, token.Kind()) ||
                token.Kind() is SyntaxKind.EndOfFileToken or SyntaxKind.None ||
                documentSyntax.SyntaxTree.IsInNonUserCode(caretPosition, cancellationToken))
            {
                return false;
            }
 
            // If the token is a )  we only want to format if it's the close paren
            // of a using statement.  That way if we have nested usings, the inner
            // using will align with the outer one when the user types the close paren.
            if (token.IsKind(SyntaxKind.CloseParenToken) && !token.Parent.IsKind(SyntaxKind.UsingStatement))
            {
                return false;
            }
 
            // If the token is a :  we only want to format if it's a labeled statement
            // or case.  When the colon is typed we'll want ot immediately have those
            // statements snap to their appropriate indentation level.
            if (token.IsKind(SyntaxKind.ColonToken) && !(token.Parent.IsKind(SyntaxKind.LabeledStatement) || token.Parent is SwitchLabelSyntax))
            {
                return false;
            }
 
            // Only format an { if it is the first token on a line.  We don't want to 
            // mess with it if it's inside a line.
            if (token.IsKind(SyntaxKind.OpenBraceToken))
            {
                if (!token.IsFirstTokenOnLine(documentSyntax.Text))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        public ImmutableArray<TextChange> GetFormattingChangesOnTypedCharacter(
            ParsedDocument document,
            int caretPosition,
            IndentationOptions indentationOptions,
            CancellationToken cancellationToken)
        {
            var root = document.Root;
            var token = root.FindToken(Math.Max(0, caretPosition - 1), findInsideTrivia: true);
            var formattingRules = GetFormattingRules(document, caretPosition, token);
 
            // Do not attempt to format on open/close brace if autoformat on close brace feature is
            // off, instead just smart indent.
            //
            // We want this behavior because it's totally reasonable for a user to want to not have
            // on automatic formatting because they feel it is too aggressive.  However, by default,
            // if you have smart-indentation on and are just hitting enter, you'll common have the
            // caret placed one indent higher than your current construct.  For example, if you have:
            //
            //      if (true)
            //          $ <-- smart indent will have placed the caret here here.
            //
            // This is acceptable given that the user may want to just write a simple statement there.
            // However, if they start writing `{`, then things should snap over to be:
            //
            //      if (true)
            //      {
            //
            // Importantly, this is just an indentation change, no actual 'formatting' is done.  We do
            // the same with close brace.  If you have:
            //
            //      if (...)
            //      {
            //          bad . ly ( for (mmated+code) )  ;
            //          $ <-- smart indent will have placed the care here.
            //
            // If the user hits `}` then we will properly smart indent the `}` to match the `{`.
            // However, we won't touch any of the other code in that block, unlike if we were
            // formatting.
            var onlySmartIndent =
                (token.IsKind(SyntaxKind.CloseBraceToken) && OnlySmartIndentCloseBrace(indentationOptions.AutoFormattingOptions)) ||
                (token.IsKind(SyntaxKind.OpenBraceToken) && OnlySmartIndentOpenBrace(indentationOptions.AutoFormattingOptions));
 
            if (onlySmartIndent)
            {
                // if we're only doing smart indent, then ignore all edits to this token that occur before
                // the span of the token. They're irrelevant and may screw up other code the user doesn't 
                // want touched.
                var tokenEdits = FormatToken(document, indentationOptions, token, formattingRules, cancellationToken);
                return tokenEdits.Where(t => t.Span.Start >= token.FullSpan.Start).ToImmutableArray();
            }
 
            // if formatting range fails, do format token one at least
            var changes = FormatRange(document, indentationOptions, token, formattingRules, cancellationToken);
            if (changes.Length > 0)
            {
                return changes;
            }
 
            return FormatToken(document, indentationOptions, token, formattingRules, cancellationToken).ToImmutableArray();
        }
 
        private static bool OnlySmartIndentCloseBrace(in AutoFormattingOptions options)
        {
            // User does not want auto-formatting (either in general, or for close braces in
            // specific).  So we only smart indent close braces when typed.
            return !options.FormatOnCloseBrace || !options.FormatOnTyping;
        }
 
        private static bool OnlySmartIndentOpenBrace(in AutoFormattingOptions options)
        {
            // User does not want auto-formatting .  So we only smart indent open braces when typed.
            // Note: there is no specific option for controlling formatting on open brace.  So we
            // don't have the symmetry with OnlySmartIndentCloseBrace.
            return !options.FormatOnTyping;
        }
 
        private static IList<TextChange> FormatToken(
            ParsedDocument document, IndentationOptions options, SyntaxToken token, ImmutableArray<AbstractFormattingRule> formattingRules, CancellationToken cancellationToken)
        {
            var formatter = new CSharpSmartTokenFormatter(options, formattingRules, (CompilationUnitSyntax)document.Root, document.Text);
            return formatter.FormatToken(token, cancellationToken);
        }
 
        private static ImmutableArray<TextChange> FormatRange(
            ParsedDocument document,
            IndentationOptions options,
            SyntaxToken endToken,
            ImmutableArray<AbstractFormattingRule> formattingRules,
            CancellationToken cancellationToken)
        {
            if (!IsEndToken(endToken))
            {
                return ImmutableArray<TextChange>.Empty;
            }
 
            var tokenRange = FormattingRangeHelper.FindAppropriateRange(endToken);
            if (tokenRange == null || tokenRange.Value.Item1.Equals(tokenRange.Value.Item2))
            {
                return ImmutableArray<TextChange>.Empty;
            }
 
            if (IsInvalidTokenKind(tokenRange.Value.Item1) || IsInvalidTokenKind(tokenRange.Value.Item2))
            {
                return ImmutableArray<TextChange>.Empty;
            }
 
            var formatter = new CSharpSmartTokenFormatter(options, formattingRules, (CompilationUnitSyntax)document.Root, document.Text);
 
            var changes = formatter.FormatRange(tokenRange.Value.Item1, tokenRange.Value.Item2, cancellationToken);
            return changes.ToImmutableArray();
        }
 
        private static IEnumerable<AbstractFormattingRule> GetTypingRules(SyntaxToken tokenBeforeCaret)
        {
            // Typing introduces several challenges around formatting.  
            // Historically we've shipped several triggers that cause formatting to happen directly while typing.
            // These include formatting of blocks when '}' is typed, formatting of statements when a ';' is typed, formatting of ```case```s when ':' typed, and many other cases.  
            // However, formatting during typing can potentially cause problems.  This is because the surrounding code may not be complete, 
            // or may otherwise have syntax errors, and thus edits could have unintended consequences.
            // 
            // Because of this, we introduce an extra rule into the set of formatting rules whose purpose is to actually make formatting *more* 
            // conservative and *less* willing willing to make edits to the tree. 
            // The primary effect this rule has is to assume that more code is on a single line (and thus should stay that way) 
            // despite what the tree actually looks like.
            // 
            // It's ok that this is only during formatting that is caused by an edit because that formatting happens 
            // implicitly and thus has to be more careful, whereas an explicit format-document call only happens on-demand 
            // and can be more aggressive about what it's doing.
            // 
            // 
            // For example, say you have the following code.
            // 
            // ```c#
            // class C
            // {
            //   int P { get {    return
            // }
            // ```
            // 
            // Hitting ';' after 'return' should ideally only affect the 'return statement' and change it to:
            // 
            // ```c#
            // class C
            // {
            //   int P { get { return;
            // }
            // ```
            // 
            // During a normal format-document call, this is not what would happen. 
            // Specifically, because the parser will consume the '}' into the accessor, 
            // it will think the accessor spans multiple lines, and thus should not stay on a single line.  This will produce:
            // 
            // ```c#
            // class C
            // {
            //   int P
            //   {
            //     get
            //     {
            //       return;
            //     }
            // ```
            // 
            // Because it's ok for this to format in that fashion if format-document is invoked, 
            // but should not happen during typing, we insert a specialized rule *only* during typing to try to control this.  
            // During normal formatting we add 'keep on single line' suppression rules for blocks we find that are on a single line.  
            // But that won't work since this span is not on a single line:
            // 
            // ```c#
            // class C
            // {
            //   int P { get [|{    return;
            // }|]
            // ```
            // 
            // So, during typing, if we see any parent block is incomplete, we'll assume that 
            // all our parent blocks are incomplete and we will place the suppression span like so:
            // 
            // ```c#
            // class C
            // {
            //   int P { get [|{     return;|]
            // }
            // ```
            // 
            // This will have the desired effect of keeping these tokens on the same line, but only during typing scenarios.  
            if (tokenBeforeCaret.Kind() is SyntaxKind.CloseBraceToken or
                SyntaxKind.EndOfFileToken)
            {
                return SpecializedCollections.EmptyEnumerable<AbstractFormattingRule>();
            }
 
            return SpecializedCollections.SingletonEnumerable(TypingFormattingRule.Instance);
        }
 
        private static bool IsEndToken(SyntaxToken endToken)
        {
            if (endToken.IsKind(SyntaxKind.OpenBraceToken))
            {
                return false;
            }
 
            return true;
        }
 
        // We'll autoformat on n, t, e, only if they are the last character of the below
        // keywords.  
        private static bool ValidSingleOrMultiCharactersTokenKind(char typedChar, SyntaxKind kind)
            => typedChar switch
            {
                'n' => kind is SyntaxKind.RegionKeyword or SyntaxKind.EndRegionKeyword,
                't' => kind == SyntaxKind.SelectKeyword,
                'e' => kind == SyntaxKind.WhereKeyword,
                _ => true,
            };
 
        private static bool IsInvalidTokenKind(SyntaxToken token)
        {
            // invalid token to be formatted
            return token.IsKind(SyntaxKind.None) ||
                   token.IsKind(SyntaxKind.EndOfDirectiveToken) ||
                   token.IsKind(SyntaxKind.EndOfFileToken);
        }
 
        private ImmutableArray<AbstractFormattingRule> GetFormattingRules(ParsedDocument document, int position, SyntaxToken tokenBeforeCaret)
        {
            var formattingRuleFactory = _services.SolutionServices.GetRequiredService<IHostDependentFormattingRuleFactoryService>();
            return ImmutableArray.Create(formattingRuleFactory.CreateRule(document, position))
                                 .AddRange(GetTypingRules(tokenBeforeCaret))
                                 .AddRange(Formatter.GetDefaultFormattingRules(_services));
        }
 
        public ImmutableArray<TextChange> GetFormattingChangesOnPaste(ParsedDocument document, TextSpan textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken)
        {
            var formattingSpan = CommonFormattingHelpers.GetFormattingSpan(document.Root, textSpan);
            var service = _services.GetRequiredService<ISyntaxFormattingService>();
 
            var rules = new List<AbstractFormattingRule>() { new PasteFormattingRule() };
            rules.AddRange(service.GetDefaultFormattingRules());
 
            var result = service.GetFormattingResult(document.Root, SpecializedCollections.SingletonEnumerable(formattingSpan), options, rules, cancellationToken);
            return result.GetTextChanges(cancellationToken).ToImmutableArray();
        }
 
        internal sealed class PasteFormattingRule : AbstractFormattingRule
        {
            public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation)
            {
                if (currentToken.Parent != null)
                {
                    var currentTokenParentParent = currentToken.Parent.Parent;
                    if (currentToken.Kind() == SyntaxKind.OpenBraceToken && currentTokenParentParent != null &&
                        (currentTokenParentParent.Kind() == SyntaxKind.SimpleLambdaExpression ||
                         currentTokenParentParent.Kind() == SyntaxKind.ParenthesizedLambdaExpression ||
                         currentTokenParentParent.Kind() == SyntaxKind.AnonymousMethodExpression))
                    {
                        return FormattingOperations.CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines);
                    }
                }
 
                return nextOperation.Invoke(in previousToken, in currentToken);
            }
        }
    }
}