|
// 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;
}
}
}
}
|