|
// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Formatting;
internal abstract partial class AbstractFormatEngine
{
/// <summary>
/// this actually applies formatting operations to trivia between two tokens
/// </summary>
private sealed class OperationApplier(FormattingContext context, ChainedFormattingRules formattingRules)
{
public bool Apply(AdjustSpacesOperation operation, int pairIndex)
{
if (operation.Option == AdjustSpacesOption.PreserveSpaces)
{
return ApplyPreserveSpacesOperation(operation, pairIndex);
}
if (operation.Option == AdjustSpacesOption.ForceSpaces)
{
return ApplyForceSpacesOperation(operation, pairIndex);
}
if (operation.Option == AdjustSpacesOption.DynamicSpaceToIndentationIfOnSingleLine)
{
return ApplyDynamicSpacesOperation(operation, pairIndex);
}
return ApplySpaceIfSingleLine(operation, pairIndex);
}
private bool ApplyDynamicSpacesOperation(AdjustSpacesOperation operation, int pairIndex)
{
var triviaInfo = context.TokenStream.GetTriviaData(pairIndex);
if (triviaInfo.SecondTokenIsFirstTokenOnLine)
{
return false;
}
Contract.ThrowIfFalse(triviaInfo.LineBreaks == 0);
var indentation = context.GetBaseIndentation(context.TokenStream.GetToken(pairIndex + 1));
var previousToken = context.TokenStream.GetToken(pairIndex);
context.TokenStream.GetTokenLength(previousToken, out var tokenLength, out var multipleLines);
// get end column of previous token
var endColumnOfPreviousToken = multipleLines ? tokenLength : context.TokenStream.GetCurrentColumn(previousToken) + tokenLength;
// check whether current position is less than indentation
if (endColumnOfPreviousToken < indentation)
{
Debug.Assert(!context.IsFormattingDisabled(pairIndex));
context.TokenStream.ApplyChange(pairIndex, triviaInfo.WithSpace(indentation - endColumnOfPreviousToken, context, formattingRules));
return true;
}
// delegate to normal single-line space applier
return ApplySpaceIfSingleLine(operation, pairIndex);
}
private bool ApplyPreserveSpacesOperation(AdjustSpacesOperation operation, int pairIndex)
{
var triviaInfo = context.TokenStream.GetTriviaData(pairIndex);
var space = operation.Space;
if (triviaInfo.SecondTokenIsFirstTokenOnLine)
{
return false;
}
Contract.ThrowIfFalse(triviaInfo.LineBreaks == 0);
if (space <= triviaInfo.Spaces)
{
return false;
}
Debug.Assert(!context.IsFormattingDisabled(pairIndex));
context.TokenStream.ApplyChange(pairIndex, triviaInfo.WithSpace(space, context, formattingRules));
return true;
}
public bool ApplyForceSpacesOperation(AdjustSpacesOperation operation, int pairIndex)
{
var triviaInfo = context.TokenStream.GetTriviaData(pairIndex);
if (triviaInfo.LineBreaks == 0 && triviaInfo.Spaces == operation.Space)
{
return false;
}
Debug.Assert(!context.IsFormattingDisabled(pairIndex));
context.TokenStream.ApplyChange(pairIndex, triviaInfo.WithSpace(operation.Space, context, formattingRules));
return true;
}
private bool ApplySpaceIfSingleLine(AdjustSpacesOperation operation, int pairIndex)
{
var triviaInfo = context.TokenStream.GetTriviaData(pairIndex);
var space = operation.Space;
if (triviaInfo.SecondTokenIsFirstTokenOnLine)
{
return false;
}
Contract.ThrowIfFalse(triviaInfo.LineBreaks == 0);
if (triviaInfo.Spaces == space)
{
return false;
}
Debug.Assert(!context.IsFormattingDisabled(pairIndex));
context.TokenStream.ApplyChange(pairIndex, triviaInfo.WithSpace(space, context, formattingRules));
return true;
}
public bool Apply(AdjustNewLinesOperation operation, int pairIndex, CancellationToken cancellationToken)
{
if (operation.Option == AdjustNewLinesOption.PreserveLines)
{
return ApplyPreserveLinesOperation(operation, pairIndex, cancellationToken);
}
else if (operation.Option == AdjustNewLinesOption.ForceLines)
{
return ApplyForceLinesOperation(operation, pairIndex, cancellationToken);
}
else
{
Debug.Assert(operation.Option == AdjustNewLinesOption.ForceLinesIfOnSingleLine);
// We force the tokens to the different line only they are on the same line
// else we leave the tokens as it is (Note: We should not preserve too. If we
// we do, then that will be counted as a line operation and the indentation of
// the second token will be modified)
if (context.TokenStream.TwoTokensOnSameLine(context.TokenStream.GetToken(pairIndex),
context.TokenStream.GetToken(pairIndex + 1)))
{
return ApplyForceLinesOperation(operation, pairIndex, cancellationToken);
}
else
{
return false;
}
}
}
private bool ApplyForceLinesOperation(AdjustNewLinesOperation operation, int pairIndex, CancellationToken cancellationToken)
{
var triviaInfo = context.TokenStream.GetTriviaData(pairIndex);
var indentation = context.GetBaseIndentation(context.TokenStream.GetToken(pairIndex + 1));
if (triviaInfo.LineBreaks == operation.Line && triviaInfo.Spaces == indentation && !triviaInfo.TreatAsElastic)
{
// things are already in the shape we want, so we don't actually need to do
// anything but, conceptually, we handled this case
return true;
}
Debug.Assert(!context.IsFormattingDisabled(pairIndex));
// well, force it regardless original content
context.TokenStream.ApplyChange(pairIndex, triviaInfo.WithLine(operation.Line, indentation, context, formattingRules, cancellationToken));
return true;
}
public bool ApplyPreserveLinesOperation(
AdjustNewLinesOperation operation, int pairIndex, CancellationToken cancellationToken)
{
var triviaInfo = context.TokenStream.GetTriviaData(pairIndex);
// okay, check whether there is line between token more than we want
// check whether we should force it if it is less than given number
var indentation = context.GetBaseIndentation(context.TokenStream.GetToken(pairIndex + 1));
if (operation.Line > triviaInfo.LineBreaks)
{
Debug.Assert(!context.IsFormattingDisabled(pairIndex));
// alright force them
context.TokenStream.ApplyChange(pairIndex, triviaInfo.WithLine(operation.Line, indentation, context, formattingRules, cancellationToken));
return true;
}
// lines between tokens are as expected, but indentation is not right
if (triviaInfo.SecondTokenIsFirstTokenOnLine &&
indentation != triviaInfo.Spaces)
{
// Formatting can only be disabled for entire lines. This block only modifies the line containing
// the second token of the current pair, so we only need to check for disabled formatting at the
// starting position of the second token of the pair.
Debug.Assert(!context.IsFormattingDisabled(new TextSpan(context.TokenStream.GetToken(pairIndex + 1).SpanStart, 0)));
context.TokenStream.ApplyChange(pairIndex, triviaInfo.WithIndentation(indentation, context, formattingRules, cancellationToken));
return true;
}
// if PreserveLineOperation's line is set to 0, let space operation to override wrapping operation
return operation.Line > 0;
}
private bool CanAlignBeApplied(
SyntaxToken token,
IEnumerable<SyntaxToken> operationTokens,
[NotNullWhen(true)] out IList<TokenData>? tokenData)
{
// if there are no tokens to align, or no visible
// base token to be aligned to, then don't do anything
if (token.Width() <= 0 || operationTokens.IsEmpty())
{
tokenData = null;
return false;
}
tokenData = GetTokenWithIndices(operationTokens);
// no valid tokens. do nothing and return
if (tokenData.Count == 0)
{
return false;
}
return true;
}
private bool ApplyAlignment(
SyntaxToken token,
IEnumerable<SyntaxToken> tokens,
Dictionary<SyntaxToken, int> previousChangesMap,
[NotNullWhen(true)] out IList<TokenData>? tokenData,
CancellationToken cancellationToken)
{
if (!CanAlignBeApplied(token, tokens, out tokenData))
{
return false;
}
ApplyIndentationToAlignWithGivenToken(token, tokenData, previousChangesMap, cancellationToken);
return true;
}
public bool ApplyAlignment(
AlignTokensOperation operation, Dictionary<SyntaxToken, int> previousChangesMap, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(previousChangesMap);
IList<TokenData>? tokenData;
switch (operation.Option)
{
case AlignTokensOption.AlignIndentationOfTokensToBaseToken:
if (!ApplyAlignment(operation.BaseToken, operation.Tokens, previousChangesMap, out tokenData, cancellationToken))
{
return false;
}
break;
case AlignTokensOption.AlignIndentationOfTokensToFirstTokenOfBaseTokenLine:
if (!ApplyAlignment(context.TokenStream.FirstTokenOfBaseTokenLine(operation.BaseToken), operation.Tokens, previousChangesMap, out tokenData, cancellationToken))
{
return false;
}
break;
default:
throw ExceptionUtilities.UnexpectedValue(operation.Option);
}
ApplyIndentationChangesToDependentTokens(tokenData, previousChangesMap, cancellationToken);
return true;
}
private void ApplyIndentationToAlignWithGivenToken(
SyntaxToken token,
IList<TokenData> list,
Dictionary<SyntaxToken, int> previousChangesMap,
CancellationToken cancellationToken)
{
// rather than having external new changes map, having snapshot concept
// in token stream might be easier to understand.
var baseSpaceOrIndentation = context.TokenStream.GetCurrentColumn(token);
for (var i = 0; i < list.Count; i++)
{
var currentToken = list[i];
var previousToken = context.TokenStream.GetPreviousTokenData(currentToken);
var triviaInfo = context.TokenStream.GetTriviaData(previousToken, currentToken);
if (!triviaInfo.SecondTokenIsFirstTokenOnLine)
{
continue;
}
ApplyIndentationToGivenPosition(
previousToken, currentToken, triviaInfo, baseSpaceOrIndentation, previousChangesMap, cancellationToken);
}
}
private void ApplyIndentationToGivenPosition(
TokenData previousToken,
TokenData currentToken,
TriviaData triviaInfo,
int baseSpaceOrIndentation,
Dictionary<SyntaxToken, int> previousChangesMap,
CancellationToken cancellationToken)
{
// add or replace existing value. this could happen if a token get moved multiple times
// due to one being involved in multiple alignment operations
previousChangesMap[currentToken.Token] = triviaInfo.Spaces;
if (previousToken.IndexInStream < 0 || triviaInfo.Spaces == baseSpaceOrIndentation)
{
return;
}
// before make any change, check whether spacing is allowed
var spanBetweenTokens = TextSpan.FromBounds(previousToken.Token.Span.End, currentToken.Token.SpanStart);
if (context.IsSpacingSuppressed(spanBetweenTokens, triviaInfo.TreatAsElastic))
{
return;
}
if (context.IsFormattingDisabled(spanBetweenTokens))
{
return;
}
// okay, update indentation
context.TokenStream.ApplyChange(
previousToken.IndexInStream,
triviaInfo.WithIndentation(baseSpaceOrIndentation, context, formattingRules, cancellationToken));
}
private IList<TokenData> GetTokenWithIndices(IEnumerable<SyntaxToken> tokens)
{
var list = new List<TokenData>();
foreach (var token in tokens)
{
// if the token is invisible or not exist, skip it.
if (token.RawKind == 0 || token.Width() <= 0)
{
continue;
}
var tokenWithIndex = context.TokenStream.GetTokenData(token);
if (tokenWithIndex.IndexInStream < 0)
{
// this token is not inside of the formatting span, ignore
continue;
}
list.Add(tokenWithIndex);
}
list.Sort((t1, t2) => t1.IndexInStream - t2.IndexInStream);
return list;
}
private bool ApplyIndentationChangesToDependentTokens(
IList<TokenData> tokenWithIndices, Dictionary<SyntaxToken, int> newChangesMap, CancellationToken cancellationToken)
{
for (var i = 0; i < tokenWithIndices.Count; i++)
{
var firstToken = tokenWithIndices[i];
// first check whether the token moved by alignment operation have affected an anchor token. if it has,
// then find the last token of that anchor span.
var endAnchorToken = context.GetEndTokenForAnchorSpan(firstToken);
if (endAnchorToken.RawKind == 0)
{
// this means given token is not anchor token, no need to do anything
continue;
}
// first token was anchor token, now find last token with index
var lastToken = context.TokenStream.GetTokenData(endAnchorToken);
if (lastToken.IndexInStream < 0)
{
lastToken = context.TokenStream.LastTokenInStream;
}
ApplyBaseTokenIndentationChangesFromTo(firstToken, firstToken, lastToken, newChangesMap, cancellationToken);
}
return true;
}
private void ApplyIndentationDeltaFromTo(
TokenData firstToken,
TokenData lastToken,
int indentationDelta,
Dictionary<SyntaxToken, int> previousChangesMap,
CancellationToken cancellationToken)
{
// can this run parallel? at least finding out all first token on line.
for (var pairIndex = firstToken.IndexInStream; pairIndex < lastToken.IndexInStream; pairIndex++)
{
var triviaInfo = context.TokenStream.GetTriviaData(pairIndex);
if (!triviaInfo.SecondTokenIsFirstTokenOnLine)
{
continue;
}
// spacing is suppressed. don't change any spacing
if (context.IsSpacingSuppressed(pairIndex))
{
continue;
}
// bail fast here.
// if an entity is in the map, then it means indentation has been applied to the token pair already.
// no reason to do same work again.
var currentToken = context.TokenStream.GetToken(pairIndex + 1);
if (previousChangesMap.ContainsKey(currentToken))
{
continue;
}
this.ApplyIndentationDelta(pairIndex, currentToken, indentationDelta, triviaInfo, previousChangesMap, cancellationToken);
}
}
private void ApplyIndentationDelta(
int pairIndex,
SyntaxToken currentToken,
int indentationDelta,
TriviaData triviaInfo,
Dictionary<SyntaxToken, int> previousChangesMap,
CancellationToken cancellationToken)
{
Contract.ThrowIfFalse(triviaInfo.SecondTokenIsFirstTokenOnLine);
var indentation = triviaInfo.Spaces + indentationDelta;
if (triviaInfo.Spaces == indentation)
{
// indentation didn't actually move. nothing to change
return;
}
Debug.Assert(!context.IsFormattingDisabled(pairIndex));
// record the fact that this pair has been moved
Debug.Assert(!previousChangesMap.ContainsKey(currentToken));
previousChangesMap.Add(currentToken, triviaInfo.Spaces);
// okay, update indentation
context.TokenStream.ApplyChange(pairIndex, triviaInfo.WithIndentation(indentation, context, formattingRules, cancellationToken));
}
public bool ApplyBaseTokenIndentationChangesFromTo(
SyntaxToken baseToken,
SyntaxToken startToken,
SyntaxToken endToken,
Dictionary<SyntaxToken, int> previousChangesMap,
CancellationToken cancellationToken)
{
Contract.ThrowIfFalse(baseToken.RawKind != 0 && startToken.RawKind != 0 && endToken.RawKind != 0);
var baseTokenWithIndex = context.TokenStream.GetTokenData(baseToken);
var firstTokenWithIndex = context.TokenStream.GetTokenData(startToken).GetPreviousTokenData();
var lastTokenWithIndex = context.TokenStream.GetTokenData(endToken);
return ApplyBaseTokenIndentationChangesFromTo(
baseTokenWithIndex, firstTokenWithIndex, lastTokenWithIndex, previousChangesMap, cancellationToken);
}
private bool ApplyBaseTokenIndentationChangesFromTo(
TokenData baseToken,
TokenData startToken,
TokenData endToken,
Dictionary<SyntaxToken, int> previousChangesMap,
CancellationToken cancellationToken)
{
// if baseToken is not in the stream, then it is guaranteed to be not moved.
var tokenWithIndex = baseToken;
if (tokenWithIndex.IndexInStream < 0)
{
return false;
}
// now, check whether tokens on that the base token depends have been moved.
// any token before the base token on the same line has implicit dependency over the base token.
while (tokenWithIndex.IndexInStream >= 0)
{
// check whether given token have moved
if (previousChangesMap.ContainsKey(tokenWithIndex.Token))
{
break;
}
// okay, this token is not moved, check one before me as long as it is on the same line
var tokenPairIndex = tokenWithIndex.IndexInStream - 1;
if (tokenPairIndex < 0 ||
context.TokenStream.GetTriviaData(tokenPairIndex).SecondTokenIsFirstTokenOnLine)
{
return false;
}
tokenWithIndex = tokenWithIndex.GetPreviousTokenData();
}
// didn't find anything moved
if (tokenWithIndex.IndexInStream < 0)
{
return false;
}
// we are not moved
var indentationDelta = context.GetDeltaFromPreviousChangesMap(tokenWithIndex.Token, previousChangesMap);
if (indentationDelta == 0)
{
return false;
}
startToken = startToken.IndexInStream < 0 ? context.TokenStream.FirstTokenInStream : startToken;
endToken = endToken.IndexInStream < 0 ? context.TokenStream.LastTokenInStream : endToken;
ApplyIndentationDeltaFromTo(startToken, endToken, indentationDelta, previousChangesMap, cancellationToken);
return true;
}
public bool ApplyAnchorIndentation(
int pairIndex, Dictionary<SyntaxToken, int> previousChangesMap, CancellationToken cancellationToken)
{
var triviaInfo = context.TokenStream.GetTriviaData(pairIndex);
if (!triviaInfo.SecondTokenIsFirstTokenOnLine)
{
return false;
}
// don't apply anchor is spacing is suppressed
if (context.IsSpacingSuppressed(pairIndex))
{
return false;
}
var firstTokenOnLine = context.TokenStream.GetToken(pairIndex + 1);
var indentation = triviaInfo.Spaces + context.GetAnchorDeltaFromOriginalColumn(firstTokenOnLine);
if (triviaInfo.Spaces != indentation)
{
// first save previous information
previousChangesMap.Add(firstTokenOnLine, triviaInfo.Spaces);
// okay, update indentation
context.TokenStream.ApplyChange(pairIndex, triviaInfo.WithIndentation(indentation, context, formattingRules, cancellationToken));
return true;
}
return false;
}
}
}
|