File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\Formatting\Rules\QueryExpressionFormattingRule.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 Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.CSharp.Formatting;
 
internal sealed class QueryExpressionFormattingRule : BaseFormattingRule
{
    internal const string Name = "CSharp Query Expressions Formatting Rule";
 
    private readonly CSharpSyntaxFormattingOptions _options;
 
    public QueryExpressionFormattingRule()
        : this(CSharpSyntaxFormattingOptions.Default)
    {
    }
 
    private QueryExpressionFormattingRule(CSharpSyntaxFormattingOptions options)
    {
        _options = options;
    }
 
    public override AbstractFormattingRule WithOptions(SyntaxFormattingOptions options)
    {
        var newOptions = options as CSharpSyntaxFormattingOptions ?? CSharpSyntaxFormattingOptions.Default;
 
        if (_options.NewLines.HasFlag(NewLinePlacement.BetweenQueryExpressionClauses) == newOptions.NewLines.HasFlag(NewLinePlacement.BetweenQueryExpressionClauses))
        {
            return this;
        }
 
        return new QueryExpressionFormattingRule(newOptions);
    }
 
    public override void AddSuppressOperations(ArrayBuilder<SuppressOperation> list, SyntaxNode node, in NextSuppressOperationAction nextOperation)
    {
        nextOperation.Invoke();
 
        if (node is QueryExpressionSyntax queryExpression)
        {
            AddSuppressWrappingIfOnSingleLineOperation(list, queryExpression.GetFirstToken(includeZeroWidth: true), queryExpression.GetLastToken(includeZeroWidth: true));
        }
    }
 
    private static void AddIndentBlockOperationsForFromClause(List<IndentBlockOperation> list, FromClauseSyntax fromClause)
    {
        // Only add the indent block operation if the 'in' keyword is present. Otherwise, we'll get the following:
        //
        //     from x
        //         in args
        //
        // Rather than:
        //
        //     from x
        //     in args
        //
        // However, we want to get the following result if the 'in' keyword is present to allow nested queries
        // to be formatted properly.
        //
        //     from x in
        //         args
 
        if (fromClause.InKeyword.IsMissing)
        {
            return;
        }
 
        var baseToken = fromClause.FromKeyword;
        var startToken = fromClause.Expression.GetFirstToken(includeZeroWidth: true);
        var endToken = fromClause.Expression.GetLastToken(includeZeroWidth: true);
 
        AddIndentBlockOperation(list, baseToken, startToken, endToken);
    }
 
    public override void AddIndentBlockOperations(List<IndentBlockOperation> list, SyntaxNode node, in NextIndentBlockOperationAction nextOperation)
    {
        nextOperation.Invoke();
 
        if (node is QueryExpressionSyntax queryExpression)
        {
            AddIndentBlockOperationsForFromClause(list, queryExpression.FromClause);
 
            foreach (var queryClause in queryExpression.Body.Clauses)
            {
                // if it is nested query expression
                if (queryClause is FromClauseSyntax fromClause)
                {
                    AddIndentBlockOperationsForFromClause(list, fromClause);
                }
            }
 
            // set alignment line for query expression
            var baseToken = queryExpression.GetFirstToken(includeZeroWidth: true);
            var endToken = queryExpression.GetLastToken(includeZeroWidth: true);
            if (!baseToken.IsMissing && !baseToken.Equals(endToken))
            {
                var startToken = baseToken.GetNextToken(includeZeroWidth: true);
                SetAlignmentBlockOperation(list, baseToken, startToken, endToken);
            }
        }
    }
 
    public override void AddAnchorIndentationOperations(List<AnchorIndentationOperation> list, SyntaxNode node, in NextAnchorIndentationOperationAction nextOperation)
    {
        nextOperation.Invoke();
        switch (node)
        {
            case QueryClauseSyntax queryClause:
                {
                    var firstToken = queryClause.GetFirstToken(includeZeroWidth: true);
                    AddAnchorIndentationOperation(list, firstToken, queryClause.GetLastToken(includeZeroWidth: true));
                    return;
                }
 
            case SelectOrGroupClauseSyntax selectOrGroupClause:
                {
                    var firstToken = selectOrGroupClause.GetFirstToken(includeZeroWidth: true);
                    AddAnchorIndentationOperation(list, firstToken, selectOrGroupClause.GetLastToken(includeZeroWidth: true));
                    return;
                }
 
            case QueryContinuationSyntax continuation:
                AddAnchorIndentationOperation(list, continuation.IntoKeyword, continuation.GetLastToken(includeZeroWidth: true));
                return;
        }
    }
 
    public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation)
    {
        if (previousToken.IsNestedQueryExpression())
        {
            return CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines);
        }
 
        // skip the very first from keyword
        if (currentToken.IsFirstFromKeywordInExpression())
        {
            return nextOperation.Invoke(in previousToken, in currentToken);
        }
 
        switch (currentToken.Kind())
        {
            case SyntaxKind.FromKeyword:
            case SyntaxKind.WhereKeyword:
            case SyntaxKind.LetKeyword:
            case SyntaxKind.JoinKeyword:
            case SyntaxKind.OrderByKeyword:
            case SyntaxKind.GroupKeyword:
            case SyntaxKind.SelectKeyword:
                if (currentToken.GetAncestor<QueryExpressionSyntax>() != null)
                {
                    if (_options.NewLines.HasFlag(NewLinePlacement.BetweenQueryExpressionClauses))
                    {
                        return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
                    }
                    else
                    {
                        return CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines);
                    }
                }
 
                break;
        }
 
        return nextOperation.Invoke(in previousToken, in currentToken);
    }
}