|
// 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.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.Indentation;
internal partial class CSharpIndentationService
{
protected override bool ShouldUseTokenIndenter(Indenter indenter, out SyntaxToken syntaxToken)
=> ShouldUseSmartTokenFormatterInsteadOfIndenter(
indenter.Rules, indenter.Root, indenter.LineToBeIndented, indenter.Options, out syntaxToken);
protected override ISmartTokenFormatter CreateSmartTokenFormatter(
CompilationUnitSyntax root, SourceText text, TextLine lineToBeIndented,
IndentationOptions options, AbstractFormattingRule baseIndentationRule)
{
var rules = ImmutableArray.Create(baseIndentationRule).AddRange(CSharpSyntaxFormatting.Instance.GetDefaultFormattingRules());
return new CSharpSmartTokenFormatter(options, rules, root, text);
}
protected override IndentationResult? GetDesiredIndentationWorker(Indenter indenter, SyntaxToken? tokenOpt, SyntaxTrivia? triviaOpt)
=> TryGetDesiredIndentation(indenter, triviaOpt) ??
TryGetDesiredIndentation(indenter, tokenOpt);
private static IndentationResult? TryGetDesiredIndentation(Indenter indenter, SyntaxTrivia? triviaOpt)
{
// If we have a // comment, and it's the only thing on the line, then if we hit enter, we should align to
// that. This helps for cases like:
//
// int goo; // this comment
// // continues
// // onwards
//
// The user will have to manually indent `// continues`, but we'll respect that indentation from that point on.
if (triviaOpt == null)
return null;
var trivia = triviaOpt.Value;
if (!trivia.IsSingleOrMultiLineComment() && !trivia.IsDocComment())
return null;
var line = indenter.Text.Lines.GetLineFromPosition(trivia.FullSpan.Start);
if (line.GetFirstNonWhitespacePosition() != trivia.FullSpan.Start)
return null;
// Previous line just contained this single line comment. Align us with it.
return new IndentationResult(trivia.FullSpan.Start, 0);
}
private static IndentationResult? TryGetDesiredIndentation(Indenter indenter, SyntaxToken? tokenOpt)
{
if (tokenOpt == null)
return null;
return GetIndentationBasedOnToken(indenter, tokenOpt.Value);
}
private static IndentationResult GetIndentationBasedOnToken(Indenter indenter, SyntaxToken token)
{
Contract.ThrowIfNull(indenter.Tree);
Contract.ThrowIfTrue(token.Kind() == SyntaxKind.None);
var sourceText = indenter.LineToBeIndented.Text;
RoslynDebug.AssertNotNull(sourceText);
// case: """$$
// """
if (token.IsKind(SyntaxKind.MultiLineRawStringLiteralToken))
{
var endLine = sourceText.Lines.GetLineFromPosition(token.Span.End);
// Raw string may be unterminated. So last line may just be the last line of the file, which may have
// no contents on it. In that case, just presume the minimum offset is 0.
var minimumOffset = endLine.GetFirstNonWhitespaceOffset() ?? 0;
// If possible, indent to match the indentation of the previous non-whitespace line contained in the
// same raw string. Otherwise, indent to match the ending line of the raw string.
var startLine = sourceText.Lines.GetLineFromPosition(token.SpanStart);
for (var currentLineNumber = indenter.LineToBeIndented.LineNumber - 1; currentLineNumber >= startLine.LineNumber + 1; currentLineNumber--)
{
var currentLine = sourceText.Lines[currentLineNumber];
if (currentLine.GetFirstNonWhitespaceOffset() is { } priorLineOffset)
{
if (priorLineOffset >= minimumOffset)
{
return indenter.GetIndentationOfLine(currentLine);
}
else
{
// The prior line is not sufficiently indented, so use the ending delimiter for the indent
break;
}
}
}
return indenter.GetIndentationOfLine(endLine);
}
// case 1: $"""$$
// """
// case 2: $"""
// text$$
// """
// case 3: $"""
// {value}$$
// """
if (token.Kind() is SyntaxKind.InterpolatedMultiLineRawStringStartToken or SyntaxKind.InterpolatedStringTextToken
|| token is { RawKind: (int)SyntaxKind.CloseBraceToken, Parent: InterpolationSyntax })
{
var interpolatedExpression = token.GetAncestor<InterpolatedStringExpressionSyntax>();
Contract.ThrowIfNull(interpolatedExpression);
if (interpolatedExpression.StringStartToken.IsKind(SyntaxKind.InterpolatedMultiLineRawStringStartToken))
{
var endLine = sourceText.Lines.GetLineFromPosition(interpolatedExpression.StringEndToken.Span.End);
// Raw string may be unterminated. So last line may just be the last line of the file, which may have
// no contents on it. In that case, just presume the minimum offset is 0.
var minimumOffset = endLine.GetFirstNonWhitespaceOffset() ?? 0;
// If possible, indent to match the indentation of the previous non-whitespace line contained in the
// same raw string. Otherwise, indent to match the ending line of the raw string.
var startLine = sourceText.Lines.GetLineFromPosition(interpolatedExpression.StringStartToken.SpanStart);
for (var currentLineNumber = indenter.LineToBeIndented.LineNumber - 1; currentLineNumber >= startLine.LineNumber + 1; currentLineNumber--)
{
var currentLine = sourceText.Lines[currentLineNumber];
if (!indenter.Root.FindToken(currentLine.Start, findInsideTrivia: true).IsKind(SyntaxKind.InterpolatedStringTextToken))
{
// Avoid trying to indent to match the content of an interpolation. Example:
//
// _ = $"""
// {
// 0} <-- the start of this line is not part of the text content
// """
//
continue;
}
if (currentLine.GetFirstNonWhitespaceOffset() is { } priorLineOffset)
{
if (priorLineOffset >= minimumOffset)
{
return indenter.GetIndentationOfLine(currentLine);
}
else
{
// The prior line is not sufficiently indented, so use the ending delimiter for the indent
break;
}
}
}
return indenter.GetIndentationOfLine(endLine);
}
}
// special cases
// case 1: token belongs to verbatim token literal
// case 2: $@"$${0}"
// case 3: $@"Comment$$ in-between{0}"
// case 4: $@"{0}$$"
if (token.IsVerbatimStringLiteral() ||
token.Kind() is SyntaxKind.InterpolatedVerbatimStringStartToken or SyntaxKind.InterpolatedStringTextToken ||
(token.IsKind(SyntaxKind.CloseBraceToken) && token.Parent.IsKind(SyntaxKind.Interpolation)))
{
return indenter.IndentFromStartOfLine(0);
}
// if previous statement belong to labeled statement, don't follow label's indentation
// but its previous one.
if (token.Parent is LabeledStatementSyntax || token.IsLastTokenInLabelStatement())
{
token = token.GetAncestor<LabeledStatementSyntax>()!.GetFirstToken(includeZeroWidth: true).GetPreviousToken(includeZeroWidth: true);
}
var position = indenter.GetCurrentPositionNotBelongToEndOfFileToken(indenter.LineToBeIndented.Start);
// first check operation service to see whether we can determine indentation from it
var indentation = indenter.Finder.FromIndentBlockOperations(indenter.Tree, token, position, indenter.CancellationToken);
if (indentation.HasValue)
{
return indenter.IndentFromStartOfLine(indentation.Value);
}
var alignmentTokenIndentation = indenter.Finder.FromAlignTokensOperations(indenter.Tree, token);
if (alignmentTokenIndentation.HasValue)
{
return indenter.IndentFromStartOfLine(alignmentTokenIndentation.Value);
}
// if we couldn't determine indentation from the service, use heuristic to find indentation.
// If this is the last token of an embedded statement, walk up to the top-most parenting embedded
// statement owner and use its indentation.
//
// cases:
// if (true)
// if (false)
// Goo();
//
// if (true)
// { }
if (token.IsSemicolonOfEmbeddedStatement() ||
token.IsCloseBraceOfEmbeddedBlock())
{
RoslynDebug.Assert(
token.Parent != null &&
(token.Parent.Parent is StatementSyntax || token.Parent.Parent is ElseClauseSyntax));
var embeddedStatementOwner = token.Parent.Parent;
while (embeddedStatementOwner.IsEmbeddedStatement())
{
RoslynDebug.AssertNotNull(embeddedStatementOwner.Parent);
embeddedStatementOwner = embeddedStatementOwner.Parent;
}
return indenter.GetIndentationOfLine(sourceText.Lines.GetLineFromPosition(embeddedStatementOwner.GetFirstToken(includeZeroWidth: true).SpanStart));
}
switch (token.Kind())
{
case SyntaxKind.SemicolonToken:
{
// special cases
if (token.IsSemicolonInForStatement())
{
return GetDefaultIndentationFromToken(indenter, token);
}
return indenter.IndentFromStartOfLine(indenter.Finder.GetIndentationOfCurrentPosition(indenter.Tree, token, position, indenter.CancellationToken));
}
case SyntaxKind.CloseBraceToken:
{
if (token.Parent.IsKind(SyntaxKind.AccessorList) &&
token.Parent.Parent.IsKind(SyntaxKind.PropertyDeclaration))
{
if (token.GetNextToken().IsEqualsTokenInAutoPropertyInitializers())
{
return GetDefaultIndentationFromToken(indenter, token);
}
}
return indenter.IndentFromStartOfLine(indenter.Finder.GetIndentationOfCurrentPosition(indenter.Tree, token, position, indenter.CancellationToken));
}
case SyntaxKind.OpenBraceToken:
{
return indenter.IndentFromStartOfLine(indenter.Finder.GetIndentationOfCurrentPosition(indenter.Tree, token, position, indenter.CancellationToken));
}
case SyntaxKind.ColonToken:
{
var nonTerminalNode = token.Parent;
Contract.ThrowIfNull(nonTerminalNode, @"Malformed code or bug in parser???");
if (nonTerminalNode is SwitchLabelSyntax)
{
return indenter.GetIndentationOfLine(sourceText.Lines.GetLineFromPosition(nonTerminalNode.GetFirstToken(includeZeroWidth: true).SpanStart), indenter.Options.FormattingOptions.IndentationSize);
}
goto default;
}
case SyntaxKind.CloseBracketToken:
{
var nonTerminalNode = token.Parent;
Contract.ThrowIfNull(nonTerminalNode, @"Malformed code or bug in parser???");
// if this is closing an attribute, we shouldn't indent.
if (nonTerminalNode is AttributeListSyntax)
{
return indenter.GetIndentationOfLine(sourceText.Lines.GetLineFromPosition(nonTerminalNode.GetFirstToken(includeZeroWidth: true).SpanStart));
}
goto default;
}
case SyntaxKind.XmlTextLiteralToken:
{
return indenter.GetIndentationOfLine(sourceText.Lines.GetLineFromPosition(token.SpanStart));
}
case SyntaxKind.CommaToken:
{
return GetIndentationFromCommaSeparatedList(indenter, token);
}
case SyntaxKind.CloseParenToken:
{
if (token.Parent.IsKind(SyntaxKind.ArgumentList))
{
return GetDefaultIndentationFromToken(indenter, token.Parent.GetFirstToken(includeZeroWidth: true));
}
goto default;
}
default:
{
return GetDefaultIndentationFromToken(indenter, token);
}
}
}
private static IndentationResult GetIndentationFromCommaSeparatedList(Indenter indenter, SyntaxToken token)
=> token.Parent switch
{
BaseArgumentListSyntax argument => GetIndentationFromCommaSeparatedList(indenter, argument.Arguments, token),
BaseParameterListSyntax parameter => GetIndentationFromCommaSeparatedList(indenter, parameter.Parameters, token),
TypeArgumentListSyntax typeArgument => GetIndentationFromCommaSeparatedList(indenter, typeArgument.Arguments, token),
TypeParameterListSyntax typeParameter => GetIndentationFromCommaSeparatedList(indenter, typeParameter.Parameters, token),
EnumDeclarationSyntax enumDeclaration => GetIndentationFromCommaSeparatedList(indenter, enumDeclaration.Members, token),
InitializerExpressionSyntax initializerSyntax => GetIndentationFromCommaSeparatedList(indenter, initializerSyntax.Expressions, token),
_ => GetDefaultIndentationFromToken(indenter, token),
};
private static IndentationResult GetIndentationFromCommaSeparatedList<T>(
Indenter indenter, SeparatedSyntaxList<T> list, SyntaxToken token) where T : SyntaxNode
{
var index = list.GetWithSeparators().IndexOf(token);
if (index < 0)
{
return GetDefaultIndentationFromToken(indenter, token);
}
// find node that starts at the beginning of a line
var sourceText = indenter.LineToBeIndented.Text;
RoslynDebug.AssertNotNull(sourceText);
for (var i = (index - 1) / 2; i >= 0; i--)
{
var node = list[i];
var firstToken = node.GetFirstToken(includeZeroWidth: true);
if (firstToken.IsFirstTokenOnLine(sourceText))
{
return indenter.GetIndentationOfLine(sourceText.Lines.GetLineFromPosition(firstToken.SpanStart));
}
}
// smart indenter has a special indent block rule for comma separated list, so don't
// need to add default additional space for multiline expressions
return GetDefaultIndentationFromTokenLine(indenter, token, additionalSpace: 0);
}
private static IndentationResult GetDefaultIndentationFromToken(Indenter indenter, SyntaxToken token)
{
if (IsPartOfQueryExpression(token))
{
return GetIndentationForQueryExpression(indenter, token);
}
return GetDefaultIndentationFromTokenLine(indenter, token);
}
private static IndentationResult GetIndentationForQueryExpression(Indenter indenter, SyntaxToken token)
{
// find containing non terminal node
var queryExpressionClause = GetQueryExpressionClause(token);
if (queryExpressionClause == null)
{
return GetDefaultIndentationFromTokenLine(indenter, token);
}
// find line where first token of the node is
var sourceText = indenter.LineToBeIndented.Text;
RoslynDebug.AssertNotNull(sourceText);
var firstToken = queryExpressionClause.GetFirstToken(includeZeroWidth: true);
var firstTokenLine = sourceText.Lines.GetLineFromPosition(firstToken.SpanStart);
// find line where given token is
var givenTokenLine = sourceText.Lines.GetLineFromPosition(token.SpanStart);
if (firstTokenLine.LineNumber != givenTokenLine.LineNumber)
{
// do default behavior
return GetDefaultIndentationFromTokenLine(indenter, token);
}
// okay, we are right under the query expression.
// align caret to query expression
if (firstToken.IsFirstTokenOnLine(sourceText))
{
return indenter.GetIndentationOfToken(firstToken);
}
// find query body that has a token that is a first token on the line
if (queryExpressionClause.Parent is not QueryBodySyntax queryBody)
{
return indenter.GetIndentationOfToken(firstToken);
}
// find preceding clause that starts on its own.
var clauses = queryBody.Clauses;
for (var i = clauses.Count - 1; i >= 0; i--)
{
var clause = clauses[i];
if (firstToken.SpanStart <= clause.SpanStart)
{
continue;
}
var clauseToken = clause.GetFirstToken(includeZeroWidth: true);
if (clauseToken.IsFirstTokenOnLine(sourceText))
{
return indenter.GetIndentationOfToken(clauseToken);
}
}
// no query clause start a line. use the first token of the query expression
RoslynDebug.AssertNotNull(queryBody.Parent);
return indenter.GetIndentationOfToken(queryBody.Parent.GetFirstToken(includeZeroWidth: true));
}
private static SyntaxNode? GetQueryExpressionClause(SyntaxToken token)
{
var clause = token.GetAncestors<SyntaxNode>().FirstOrDefault(n => n is QueryClauseSyntax or SelectOrGroupClauseSyntax);
if (clause != null)
{
return clause;
}
// If this is a query continuation, use the last clause of its parenting query.
var body = token.GetAncestor<QueryBodySyntax>();
if (body != null)
{
if (body.SelectOrGroup.IsMissing)
{
return body.Clauses.LastOrDefault();
}
else
{
return body.SelectOrGroup;
}
}
return null;
}
private static bool IsPartOfQueryExpression(SyntaxToken token)
{
var queryExpression = token.GetAncestor<QueryExpressionSyntax>();
return queryExpression != null;
}
private static IndentationResult GetDefaultIndentationFromTokenLine(
Indenter indenter, SyntaxToken token, int? additionalSpace = null)
{
var spaceToAdd = additionalSpace ?? indenter.Options.FormattingOptions.IndentationSize;
var sourceText = indenter.LineToBeIndented.Text;
RoslynDebug.AssertNotNull(sourceText);
// find line where given token is
var givenTokenLine = sourceText.Lines.GetLineFromPosition(token.SpanStart);
// find right position
var position = indenter.GetCurrentPositionNotBelongToEndOfFileToken(indenter.LineToBeIndented.Start);
// find containing non expression node
var nonExpressionNode = token.GetAncestors<SyntaxNode>().FirstOrDefault(n => n is StatementSyntax);
if (nonExpressionNode == null)
{
// well, I can't find any non expression node. use default behavior
return indenter.IndentFromStartOfLine(indenter.Finder.GetIndentationOfCurrentPosition(indenter.Tree, token, position, spaceToAdd, indenter.CancellationToken));
}
// find line where first token of the node is
var firstTokenLine = sourceText.Lines.GetLineFromPosition(nonExpressionNode.GetFirstToken(includeZeroWidth: true).SpanStart);
// single line expression
if (firstTokenLine.LineNumber == givenTokenLine.LineNumber)
{
return indenter.IndentFromStartOfLine(indenter.Finder.GetIndentationOfCurrentPosition(indenter.Tree, token, position, spaceToAdd, indenter.CancellationToken));
}
// okay, looks like containing node is written over multiple lines, in that case, give same indentation as given token
return indenter.GetIndentationOfLine(givenTokenLine);
}
}
|