File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Formatting\BottomUpBaseIndentationFinder.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.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.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Formatting;
 
internal sealed class BottomUpBaseIndentationFinder
{
    private readonly TokenStream? _tokenStream;
    private readonly ChainedFormattingRules _formattingRules;
    private readonly int _tabSize;
    private readonly int _indentationSize;
    private readonly IHeaderFacts _headerFacts;
 
    public BottomUpBaseIndentationFinder(
        ChainedFormattingRules formattingRules,
        int tabSize,
        int indentationSize,
        TokenStream? tokenStream,
        IHeaderFacts headerFacts)
    {
        Contract.ThrowIfNull(formattingRules);
 
        _formattingRules = formattingRules;
        _tabSize = tabSize;
        _indentationSize = indentationSize;
        _tokenStream = tokenStream;
        _headerFacts = headerFacts;
    }
 
    public int? FromIndentBlockOperations(
        SyntaxTree tree, SyntaxToken token, int position, CancellationToken cancellationToken)
    {
        // we use operation service to see whether it is a starting point of new indentation.
        // ex)
        //  if (true)
        //  {
        //     | <= this is new starting point of new indentation
        var operation = GetIndentationDataFor(tree.GetRoot(cancellationToken), token, position);
 
        // try find indentation based on indentation operation
        if (operation != null)
        {
            // make sure we found new starting point of new indentation.
            // such operation should start span after the token (a token that is right before the new indentation),
            // contains current position, and position should be before the existing next token
            if (token.Span.End <= operation.TextSpan.Start &&
                operation.TextSpan.IntersectsWith(position) &&
                position <= token.GetNextToken(includeZeroWidth: true).SpanStart)
            {
                return GetIndentationOfCurrentPosition(tree, token, position, cancellationToken);
            }
        }
 
        return null;
    }
 
    public int? FromAlignTokensOperations(SyntaxTree tree, SyntaxToken token)
    {
        // let's check whether there is any missing token under us and whether
        // there is an align token operation for that missing token.
        var nextToken = token.GetNextToken(includeZeroWidth: true);
        if (nextToken.RawKind != 0 &&
            nextToken.Width() <= 0)
        {
            // looks like we have one. find whether there is a align token operation for this token
            var alignmentBaseToken = GetAlignmentBaseTokenFor(nextToken);
            if (alignmentBaseToken.RawKind != 0)
            {
                return tree.GetTokenColumn(alignmentBaseToken, _tabSize);
            }
        }
 
        return null;
    }
 
    public int GetIndentationOfCurrentPosition(
        SyntaxTree tree, SyntaxToken token, int position, CancellationToken cancellationToken)
    {
        return GetIndentationOfCurrentPosition(tree, token, position, extraSpaces: 0, cancellationToken: cancellationToken);
    }
 
    public int GetIndentationOfCurrentPosition(
        SyntaxTree tree, SyntaxToken token, int position, int extraSpaces, CancellationToken cancellationToken)
    {
        // gather all indent operations 
        var list = GetParentIndentBlockOperations(token);
 
        return GetIndentationOfCurrentPosition(
            tree.GetRoot(cancellationToken),
            list, position, extraSpaces,
            t => tree.GetTokenColumn(t, _tabSize),
            cancellationToken);
    }
 
    public int GetIndentationOfCurrentPosition(
        SyntaxNode root,
        IndentBlockOperation startingOperation,
        Func<SyntaxToken, int> tokenColumnGetter,
        CancellationToken cancellationToken)
    {
        var token = startingOperation.StartToken;
 
        // gather all indent operations 
        var list = GetParentIndentBlockOperations(token);
 
        // remove one that is smaller than current one
        for (var i = list.Count - 1; i >= 0; i--)
        {
            if (CommonFormattingHelpers.IndentBlockOperationComparer(startingOperation, list[i]) < 0)
            {
                list.RemoveAt(i);
            }
            else
            {
                break;
            }
        }
 
        return GetIndentationOfCurrentPosition(root, list, token.SpanStart, /* extraSpaces */ 0, tokenColumnGetter, cancellationToken);
    }
 
    private int GetIndentationOfCurrentPosition(
        SyntaxNode root,
        List<IndentBlockOperation> list,
        int position,
        int extraSpaces,
        Func<SyntaxToken, int> tokenColumnGetter,
        CancellationToken cancellationToken)
    {
        var tuple = GetIndentationRuleOfCurrentPosition(root, list, position);
        var indentationLevel = tuple.indentation;
        var operation = tuple.operation;
 
        if (operation == null)
        {
            return indentationLevel * _indentationSize + extraSpaces;
        }
 
        if (operation.IsRelativeIndentation)
        {
            var baseToken = operation.BaseToken;
            RoslynDebug.AssertNotNull(baseToken.SyntaxTree);
 
            // If the SmartIndenter created this IndentationFinder then tokenStream will be a null hence we should do a null check on the tokenStream
            if (operation.Option.IsOn(IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine))
            {
                if (_tokenStream != null)
                {
                    baseToken = _tokenStream.FirstTokenOfBaseTokenLine(baseToken);
                }
                else
                {
                    var textLine = baseToken.SyntaxTree.GetText(cancellationToken).Lines.GetLineFromPosition(baseToken.SpanStart);
                    baseToken = baseToken.SyntaxTree.GetRoot(cancellationToken).FindToken(textLine.Start);
                }
            }
 
            var baseIndentation = tokenColumnGetter(baseToken);
            var delta = operation.GetAdjustedIndentationDelta(_headerFacts, root, baseToken);
            return Math.Max(0, baseIndentation + (indentationLevel + delta) * _indentationSize);
        }
 
        if (operation.Option.IsOn(IndentBlockOption.AbsolutePosition))
        {
            return Math.Max(0, indentationLevel + extraSpaces);
        }
 
        throw ExceptionUtilities.Unreachable();
    }
 
    private (int indentation, IndentBlockOperation? operation) GetIndentationRuleOfCurrentPosition(
        SyntaxNode root, List<IndentBlockOperation> list, int position)
    {
        var indentationLevel = 0;
        var operations = GetIndentBlockOperationsFromSmallestSpan(root, list, position);
        foreach (var operation in operations)
        {
            if (operation.Option.IsOn(IndentBlockOption.AbsolutePosition))
            {
                return (operation.IndentationDeltaOrPosition + _indentationSize * indentationLevel, operation);
            }
 
            if (operation.Option == IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine)
            {
                return (indentationLevel, operation);
            }
 
            if (operation.IsRelativeIndentation)
            {
                return (indentationLevel, operation);
            }
 
            // move up to its containing operation
            indentationLevel += operation.IndentationDeltaOrPosition;
        }
 
        return (indentationLevel, null);
    }
 
    private List<IndentBlockOperation> GetParentIndentBlockOperations(SyntaxToken token)
    {
        var allNodes = GetParentNodes(token);
 
        // gather all indent operations 
        var list = new List<IndentBlockOperation>();
        allNodes.Do(n => _formattingRules.AddIndentBlockOperations(list, n));
 
        // sort them in right order
        list.RemoveAll(static o => o is null);
        list.Sort(CommonFormattingHelpers.IndentBlockOperationComparer);
 
        return list;
    }
 
    // Get parent nodes, including walking out of structured trivia.
    private static IEnumerable<SyntaxNode> GetParentNodes(SyntaxToken token)
    {
        var current = token.Parent;
 
        while (current != null)
        {
            yield return current;
            if (current.IsStructuredTrivia)
            {
                current = ((IStructuredTriviaSyntax)current).ParentTrivia.Token.Parent;
            }
            else
            {
                current = current.Parent;
            }
        }
    }
 
    private SyntaxToken GetAlignmentBaseTokenFor(SyntaxToken token)
    {
        var startNode = token.Parent;
 
        var list = new List<AlignTokensOperation>();
        var currentNode = startNode;
 
        while (currentNode != null)
        {
            list.Clear();
            _formattingRules.AddAlignTokensOperations(list, currentNode);
 
            if (list.Count == 0)
            {
                currentNode = currentNode.Parent;
                continue;
            }
 
            // make sure we have the given token as one of tokens to be aligned to the base token
            var match = list.FirstOrDefault(o => o != null && o.Tokens.Contains(token));
            if (match != null)
            {
                return match.BaseToken;
            }
 
            currentNode = currentNode.Parent;
        }
 
        return default;
    }
 
    private IndentBlockOperation? GetIndentationDataFor(SyntaxNode root, SyntaxToken token, int position)
    {
        var startNode = token.Parent;
 
        // starting from given token, move up to the root until it finds the first set of appropriate operations
        var list = new List<IndentBlockOperation>();
 
        var currentNode = startNode;
        while (currentNode != null)
        {
            _formattingRules.AddIndentBlockOperations(list, currentNode);
 
            if (list.Any(o => o != null && o.TextSpan.Contains(position)))
            {
                break;
            }
 
            currentNode = currentNode.Parent;
        }
 
        // well, found no appropriate one
        list.RemoveAll(static o => o is null);
        if (list.Count == 0)
        {
            return null;
        }
 
        // now sort the found ones in right order
        list.Sort(CommonFormattingHelpers.IndentBlockOperationComparer);
 
        return GetIndentBlockOperationsFromSmallestSpan(root, list, position).FirstOrDefault();
    }
 
    private static IEnumerable<IndentBlockOperation> GetIndentBlockOperationsFromSmallestSpan(SyntaxNode root, List<IndentBlockOperation> list, int position)
    {
        var lastVisibleToken = default(SyntaxToken);
        var map = new HashSet<TextSpan>();
 
        // iterate backward
        for (var i = list.Count - 1; i >= 0; i--)
        {
            var operation = list[i];
            if (map.Contains(operation.TextSpan))
            {
                // no duplicated one
                continue;
            }
 
            map.Add(operation.TextSpan);
 
            // normal case. the operation contains the position
            if (operation.TextSpan.Contains(position))
            {
                yield return operation;
                continue;
            }
 
            // special case for empty span. in case of empty span, consider it
            // contains the position if start == position
            if (operation.TextSpan.IsEmpty && operation.TextSpan.Start == position)
            {
                yield return operation;
                continue;
            }
 
            var nextToken = operation.EndToken.GetNextToken(includeZeroWidth: true);
 
            // special case where position is same as end position of an operation and
            // its next token is missing token. in this case, we will consider current position 
            // to belong to current operation.
            // this can happen in malformed code where end of indentation is missing
            if (operation.TextSpan.End == position && nextToken.IsMissing)
            {
                yield return operation;
                continue;
            }
 
            // special case where position is same as end position of the operation and
            // its next token is right at the position
            if (operation.TextSpan.End == position && position == nextToken.SpanStart)
            {
                yield return operation;
                continue;
            }
 
            // special case for the end of the span == position
            // if position is at the end of the last token of the tree. consider the position
            // belongs to the operation
            if (root.FullSpan.End == position && operation.TextSpan.End == position)
            {
                yield return operation;
                continue;
            }
 
            // more expensive check
            lastVisibleToken = (lastVisibleToken.RawKind == 0) ? root.GetLastToken() : lastVisibleToken;
            if (lastVisibleToken.Span.End <= position && operation.TextSpan.End == position)
            {
                yield return operation;
                continue;
            }
        }
    }
}