// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; 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.LanguageServices; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace dnSpy.Roslyn.Internal.SmartIndent.CSharp { internal partial class CSharpIndentationService { internal class Indenter : AbstractIndenter { public Indenter( ISyntaxFactsService syntaxFacts, SyntaxTree syntaxTree, IEnumerable rules, OptionSet optionSet, TextLine line, CancellationToken cancellationToken) : base(syntaxFacts, syntaxTree, rules, optionSet, line, cancellationToken) { } protected override IndentationResult? GetDesiredIndentationWorker( SyntaxToken token, TextLine previousLine, int lastNonWhitespacePosition) { // okay, now check whether the text we found is trivia or actual token. if (token.Span.Contains(lastNonWhitespacePosition)) { // okay, it is a token case, do special work based on type of last token on previous line return GetIndentationBasedOnToken(token); } else { // there must be trivia that contains or touch this position Contract.Assert(token.FullSpan.Contains(lastNonWhitespacePosition)); // okay, now check whether the trivia is at the beginning of the line var firstNonWhitespacePosition = previousLine.GetFirstNonWhitespacePosition(); if (!firstNonWhitespacePosition.HasValue) { return IndentFromStartOfLine(0); } var trivia = Tree.GetRoot(CancellationToken).FindTrivia(firstNonWhitespacePosition.Value, findInsideTrivia: true); if (trivia.Kind() == SyntaxKind.None || this.LineToBeIndented.LineNumber > previousLine.LineNumber + 1) { // If the token belongs to the next statement and is also the first token of the statement, then it means the user wants // to start type a new statement. So get indentation from the start of the line but not based on the token. // Case: // static void Main(string[] args) // { // // A // // B // // $$ // return; // } var containingStatement = token.GetAncestor(); if (containingStatement != null && containingStatement.GetFirstToken() == token) { var position = GetCurrentPositionNotBelongToEndOfFileToken(LineToBeIndented.Start); return IndentFromStartOfLine(Finder.GetIndentationOfCurrentPosition(Tree, token, position, CancellationToken)); } // If the token previous of the base token happens to be a Comma from a separation list then we need to handle it different // Case: // var s = new List // { // """", // """",/*sdfsdfsdfsdf*/ // // dfsdfsdfsdfsdf // // $$ // }; var previousToken = token.GetPreviousToken(); if (previousToken.IsKind(SyntaxKind.CommaToken)) { return GetIndentationFromCommaSeparatedList(previousToken); } else if (!previousToken.IsKind(SyntaxKind.None)) { // okay, beginning of the line is not trivia, use the last token on the line as base token return GetIndentationBasedOnToken(token); } } // this case we will keep the indentation of this trivia line // this trivia can't be preprocessor by the way. return GetIndentationOfLine(previousLine); } } private IndentationResult? GetIndentationBasedOnToken(SyntaxToken token) { Contract.ThrowIfNull(Tree); Contract.ThrowIfTrue(token.Kind() == SyntaxKind.None); // special cases // case 1: token belongs to verbatim token literal // case 2: $@"$${0}" // case 3: $@"Comment$$ inbetween{0}" // case 4: $@"{0}$$" if (token.IsVerbatimStringLiteral() || token.IsKind(SyntaxKind.InterpolatedVerbatimStringStartToken) || token.IsKind(SyntaxKind.InterpolatedStringTextToken) || (token.IsKind(SyntaxKind.CloseBraceToken) && token.Parent.IsKind(SyntaxKind.Interpolation))) { return 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().GetFirstToken(includeZeroWidth: true).GetPreviousToken(includeZeroWidth: true); } var position = GetCurrentPositionNotBelongToEndOfFileToken(LineToBeIndented.Start); // first check operation service to see whether we can determine indentation from it var indentation = Finder.FromIndentBlockOperations(Tree, token, position, CancellationToken); if (indentation.HasValue) { return IndentFromStartOfLine(indentation.Value); } var alignmentTokenIndentation = Finder.FromAlignTokensOperations(Tree, token); if (alignmentTokenIndentation.HasValue) { return IndentFromStartOfLine(alignmentTokenIndentation.Value); } // if we couldn't determine indentation from the service, use heuristic to find indentation. var sourceText = LineToBeIndented.Text; // 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()) { Contract.Requires( token.Parent != null && (token.Parent.Parent is StatementSyntax || token.Parent.Parent is ElseClauseSyntax)); var embeddedStatementOwner = token.Parent.Parent; while (embeddedStatementOwner.IsEmbeddedStatement()) { embeddedStatementOwner = embeddedStatementOwner.Parent; } return GetIndentationOfLine(sourceText.Lines.GetLineFromPosition(embeddedStatementOwner.GetFirstToken(includeZeroWidth: true).SpanStart)); } switch (token.Kind()) { case SyntaxKind.SemicolonToken: { // special cases if (token.IsSemicolonInForStatement()) { return GetDefaultIndentationFromToken(token); } return IndentFromStartOfLine(Finder.GetIndentationOfCurrentPosition(Tree, token, position, CancellationToken)); } case SyntaxKind.CloseBraceToken: { if (token.Parent.IsKind(SyntaxKind.AccessorList) && token.Parent.Parent.IsKind(SyntaxKind.PropertyDeclaration)) { if (token.GetNextToken().IsEqualsTokenInAutoPropertyInitializers()) { return GetDefaultIndentationFromToken(token); } } return IndentFromStartOfLine(Finder.GetIndentationOfCurrentPosition(Tree, token, position, CancellationToken)); } case SyntaxKind.OpenBraceToken: { return IndentFromStartOfLine(Finder.GetIndentationOfCurrentPosition(Tree, token, position, CancellationToken)); } case SyntaxKind.ColonToken: { var nonTerminalNode = token.Parent; Contract.ThrowIfNull(nonTerminalNode, @"Malformed code or bug in parser???"); if (nonTerminalNode is SwitchLabelSyntax) { return GetIndentationOfLine(sourceText.Lines.GetLineFromPosition(nonTerminalNode.GetFirstToken(includeZeroWidth: true).SpanStart), OptionSet.GetOption(FormattingOptions.IndentationSize, token.Language)); } // default case return GetDefaultIndentationFromToken(token); } 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 GetIndentationOfLine(sourceText.Lines.GetLineFromPosition(nonTerminalNode.GetFirstToken(includeZeroWidth: true).SpanStart)); } // default case return GetDefaultIndentationFromToken(token); } case SyntaxKind.XmlTextLiteralToken: { return GetIndentationOfLine(sourceText.Lines.GetLineFromPosition(token.SpanStart)); } case SyntaxKind.CommaToken: { return GetIndentationFromCommaSeparatedList(token); } default: { return GetDefaultIndentationFromToken(token); } } } private IndentationResult? GetIndentationFromCommaSeparatedList(SyntaxToken token) { var node = token.Parent; switch (node) { case BaseArgumentListSyntax argument: return GetIndentationFromCommaSeparatedList(argument.Arguments, token); case BaseParameterListSyntax parameter: return GetIndentationFromCommaSeparatedList(parameter.Parameters, token); case TypeArgumentListSyntax typeArgument: return GetIndentationFromCommaSeparatedList(typeArgument.Arguments, token); case TypeParameterListSyntax typeParameter: return GetIndentationFromCommaSeparatedList(typeParameter.Parameters, token); case EnumDeclarationSyntax enumDeclaration: return GetIndentationFromCommaSeparatedList(enumDeclaration.Members, token); case InitializerExpressionSyntax initializerSyntax: return GetIndentationFromCommaSeparatedList(initializerSyntax.Expressions, token); } return GetDefaultIndentationFromToken(token); } private IndentationResult? GetIndentationFromCommaSeparatedList(SeparatedSyntaxList list, SyntaxToken token) where T : SyntaxNode { var index = list.GetWithSeparators().IndexOf(token); if (index < 0) { return GetDefaultIndentationFromToken(token); } // find node that starts at the beginning of a line var sourceText = LineToBeIndented.Text; for (int i = (index - 1) / 2; i >= 0; i--) { var node = list[i]; var firstToken = node.GetFirstToken(includeZeroWidth: true); if (firstToken.IsFirstTokenOnLine(sourceText)) { return 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(token, additionalSpace: 0); } private IndentationResult? GetDefaultIndentationFromToken(SyntaxToken token) { if (IsPartOfQueryExpression(token)) { return GetIndentationForQueryExpression(token); } return GetDefaultIndentationFromTokenLine(token); } private IndentationResult? GetIndentationForQueryExpression(SyntaxToken token) { // find containing non terminal node var queryExpressionClause = GetQueryExpressionClause(token); if (queryExpressionClause == null) { return GetDefaultIndentationFromTokenLine(token); } // find line where first token of the node is var sourceText = LineToBeIndented.Text; 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(token); } // okay, we are right under the query expression. // align caret to query expression if (firstToken.IsFirstTokenOnLine(sourceText)) { return GetIndentationOfToken(firstToken); } // find query body that has a token that is a first token on the line var queryBody = queryExpressionClause.Parent as QueryBodySyntax; if (queryBody == null) { return GetIndentationOfToken(firstToken); } // find preceding clause that starts on its own. var clauses = queryBody.Clauses; for (int 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 GetIndentationOfToken(clauseToken); } } // no query clause start a line. use the first token of the query expression return GetIndentationOfToken(queryBody.Parent.GetFirstToken(includeZeroWidth: true)); } private SyntaxNode GetQueryExpressionClause(SyntaxToken token) { var clause = token.GetAncestors().FirstOrDefault(n => n is QueryClauseSyntax || n is SelectOrGroupClauseSyntax); if (clause != null) { return clause; } // If this is a query continuation, use the last clause of its parenting query. var body = token.GetAncestor(); if (body != null) { if (body.SelectOrGroup.IsMissing) { return body.Clauses.LastOrDefault(); } else { return body.SelectOrGroup; } } return null; } private bool IsPartOfQueryExpression(SyntaxToken token) { var queryExpression = token.GetAncestor(); return queryExpression != null; } private IndentationResult? GetDefaultIndentationFromTokenLine(SyntaxToken token, int? additionalSpace = null) { var spaceToAdd = additionalSpace ?? this.OptionSet.GetOption(FormattingOptions.IndentationSize, token.Language); var sourceText = LineToBeIndented.Text; // find line where given token is var givenTokenLine = sourceText.Lines.GetLineFromPosition(token.SpanStart); // find right position var position = GetCurrentPositionNotBelongToEndOfFileToken(LineToBeIndented.Start); // find containing non expression node var nonExpressionNode = token.GetAncestors().FirstOrDefault(n => n is StatementSyntax); if (nonExpressionNode == null) { // well, I can't find any non expression node. use default behavior return IndentFromStartOfLine(Finder.GetIndentationOfCurrentPosition(Tree, token, position, spaceToAdd, 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 IndentFromStartOfLine(Finder.GetIndentationOfCurrentPosition(Tree, token, position, spaceToAdd, CancellationToken)); } // okay, looks like containing node is written over multiple lines, in that case, give same indentation as given token return GetIndentationOfLine(givenTokenLine); } } } }