' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. Imports System.Threading Imports dnSpy.Roslyn.Internal Imports dnSpy.Roslyn.Internal.SmartIndent Imports dnSpy.Roslyn.Internal.SmartIndent.AbstractIndentationService Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Formatting Imports Microsoft.CodeAnalysis.Formatting.Rules Imports Microsoft.CodeAnalysis.LanguageServices Imports Microsoft.CodeAnalysis.Options Imports Microsoft.CodeAnalysis.Text Imports Microsoft.CodeAnalysis.VisualBasic.Formatting Imports Microsoft.CodeAnalysis.VisualBasic Imports Microsoft.CodeAnalysis.VisualBasic.Extensions Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Imports Microsoft.CodeAnalysis.Shared.Extensions Namespace Global.dnSpy.Roslyn.VisualBasic.Internal.SmartIndent Partial Friend Class VisualBasicIndentationService Private Class Indenter Inherits AbstractIndenter Public Sub New(syntaxFacts As ISyntaxFactsService, syntaxTree As SyntaxTree, rules As IEnumerable(Of IFormattingRule), optionSet As OptionSet, line As TextLine, cancellationToken As CancellationToken) MyBase.New(syntaxFacts, syntaxTree, rules, optionSet, line, cancellationToken) End Sub Protected Overrides Function GetDesiredIndentationWorker( token As SyntaxToken, previousLine As TextLine, lastNonWhitespacePosition As Integer) As IndentationResult? If token.Span.End = lastNonWhitespacePosition + 1 Then Return GetIndentationBasedOnToken(token) Else 'Contract.Assert(token.FullSpan.Contains(lastNonWhitespacePosition)) Dim trivia = Tree.GetRoot(CancellationToken).FindTrivia(lastNonWhitespacePosition) ' preserve the indentation of the comment trivia before a case statement If trivia.Kind = SyntaxKind.CommentTrivia AndAlso trivia.Token.IsKind(SyntaxKind.CaseKeyword) AndAlso trivia.Token.Parent.IsKind(SyntaxKind.CaseStatement) Then Return GetIndentationOfLine(previousLine) End If If trivia.Kind = SyntaxKind.LineContinuationTrivia OrElse trivia.Kind = SyntaxKind.CommentTrivia Then Return GetIndentationBasedOnToken(GetTokenOnLeft(trivia), trivia) End If ' if we are at invalid token (skipped token) at the end of statement, treat it like we are after line continuation If trivia.Kind = SyntaxKind.SkippedTokensTrivia AndAlso trivia.Token.IsLastTokenOfStatement() Then Return GetIndentationBasedOnToken(GetTokenOnLeft(trivia), trivia) End If ' okay, now check whether the trivia is at the beginning of the line Dim firstNonWhitespacePosition = previousLine.GetFirstNonWhitespacePosition() If Not firstNonWhitespacePosition.HasValue Then Return IndentFromStartOfLine(0) End If Dim firstTokenOnLine = Tree.GetRoot(CancellationToken).FindToken(firstNonWhitespacePosition.Value, findInsideTrivia:=True) If firstTokenOnLine.Kind <> SyntaxKind.None AndAlso firstTokenOnLine.Span.Contains(firstNonWhitespacePosition.Value) Then 'okay, beginning of the line is not trivia, use this token as the base token Return GetIndentationBasedOnToken(firstTokenOnLine) End If Return GetIndentationOfLine(previousLine) End If End Function Private Function GetTokenOnLeft(trivia As SyntaxTrivia) As SyntaxToken Dim token = trivia.Token If token.Span.End <= trivia.SpanStart AndAlso Not token.IsMissing Then Return token End If Return token.GetPreviousToken() End Function Private Function GetIndentationBasedOnToken(token As SyntaxToken, Optional trivia As SyntaxTrivia = Nothing) As IndentationResult? Dim sourceText = LineToBeIndented.Text Dim position = GetCurrentPositionNotBelongToEndOfFileToken(LineToBeIndented.Start) ' lines must be blank since we got the token from the first non blank line above current position If HasLinesBetween(Tree.GetText().Lines.IndexOf(token.Span.End), LineToBeIndented.LineNumber) Then ' if there are blank lines between, return indentation of the owning statement Return GetIndentationOfCurrentPosition(token, position) End If Dim indentation = GetIndentationFromOperationService(token, position) If indentation.HasValue Then Return indentation.Value End If Dim queryNode = token.GetAncestor(Of QueryClauseSyntax)() If queryNode IsNot Nothing Then Dim subQuerySpaces = If(token.IsLastTokenOfStatement(), 0, Me.OptionSet.GetOption(FormattingOptions.IndentationSize, token.Language)) Return GetIndentationOfToken(queryNode.GetFirstToken(includeZeroWidth:=True), subQuerySpaces) End If ' check one more time for query case If token.Kind = SyntaxKind.IdentifierToken AndAlso token.HasMatchingText(SyntaxKind.FromKeyword) Then Return GetIndentationOfToken(token) End If If FormattingHelpers.IsXmlTokenInXmlDeclaration(token) Then Dim xmlDocument = token.GetAncestor(Of XmlDocumentSyntax)() Return GetIndentationOfToken(xmlDocument.GetFirstToken(includeZeroWidth:=True)) End If ' implicit line continuation case If IsLineContinuable(token, trivia, position) Then Return GetIndentationFromTokenLineAfterLineContinuation(token, trivia) End If Return GetIndentationOfCurrentPosition(token, position) End Function Private Function GetIndentationOfCurrentPosition(token As SyntaxToken, position As Integer) As IndentationResult Return GetIndentationOfCurrentPosition(token, position, extraSpaces:=0) End Function Private Function GetIndentationOfCurrentPosition(token As SyntaxToken, position As Integer, extraSpaces As Integer) As IndentationResult ' special case for multi-line string Dim containingToken = Tree.FindTokenOnLeftOfPosition(position, CancellationToken) If containingToken.IsKind(SyntaxKind.InterpolatedStringTextToken) OrElse containingToken.IsKind(SyntaxKind.InterpolatedStringText) OrElse (containingToken.IsKind(SyntaxKind.CloseBraceToken) AndAlso token.Parent.IsKind(SyntaxKind.Interpolation)) Then Return IndentFromStartOfLine(0) End If If containingToken.Kind = SyntaxKind.StringLiteralToken AndAlso containingToken.FullSpan.Contains(position) Then Return IndentFromStartOfLine(0) End If Return IndentFromStartOfLine(Finder.GetIndentationOfCurrentPosition(Tree, token, position, extraSpaces, CancellationToken)) End Function Private Function IsLineContinuable(lastVisibleTokenOnPreviousLine As SyntaxToken, trivia As SyntaxTrivia, position As Integer) As Boolean If trivia.Kind = SyntaxKind.LineContinuationTrivia OrElse trivia.Kind = SyntaxKind.SkippedTokensTrivia Then Return True End If If lastVisibleTokenOnPreviousLine.IsLastTokenOfStatement() Then Return False End If Dim visibleTokenOnCurrentLine As SyntaxToken = lastVisibleTokenOnPreviousLine.GetNextToken() If Not lastVisibleTokenOnPreviousLine.IsKind(SyntaxKind.OpenBraceToken) AndAlso Not lastVisibleTokenOnPreviousLine.IsKind(SyntaxKind.CommaToken) Then If IsCloseBraceOfInitializerSyntax(visibleTokenOnCurrentLine) Then Return False End If Else If IsCloseBraceOfInitializerSyntax(visibleTokenOnCurrentLine) Then Return True End If End If If Not ContainingStatementHasDiagnostic(lastVisibleTokenOnPreviousLine.Parent) Then Return True End If If lastVisibleTokenOnPreviousLine.GetNextToken(includeZeroWidth:=True).IsMissing Then Return True End If Return False End Function Private Function IsCloseBraceOfInitializerSyntax(visibleTokenOnCurrentLine As SyntaxToken) As Boolean If visibleTokenOnCurrentLine.IsKind(SyntaxKind.CloseBraceToken) Then Dim visibleTokenOnCurrentLineParent = visibleTokenOnCurrentLine.Parent If TypeOf visibleTokenOnCurrentLineParent Is ObjectCreationInitializerSyntax OrElse TypeOf visibleTokenOnCurrentLineParent Is CollectionInitializerSyntax Then Return True End If End If Return False End Function Private Function ContainingStatementHasDiagnostic(node As SyntaxNode) As Boolean If node Is Nothing Then Return False End If If node.ContainsDiagnostics Then Return True End If Dim containingStatement = node.GetAncestorOrThis(Of StatementSyntax)() If containingStatement Is Nothing Then Return False End If Return containingStatement.ContainsDiagnostics() End Function Private Function GetIndentationFromOperationService(token As SyntaxToken, position As Integer) As IndentationResult? ' check operation service to see whether we can determine indentation from it If token.Kind = SyntaxKind.None Then Return Nothing End If Dim indentation = Finder.FromIndentBlockOperations(Tree, token, position, CancellationToken) If indentation.HasValue Then Return IndentFromStartOfLine(indentation.Value) End If ' special case xml text literal before checking alignment operation ' VB has different behavior around missing alignment token. for query expression, VB prefers putting ' caret aligned with previous query clause, but for xml literals, it prefer them to be ignored and indented ' based on current indentation level. If token.Kind = SyntaxKind.XmlTextLiteralToken OrElse token.Kind = SyntaxKind.XmlEntityLiteralToken Then Return GetIndentationOfLine(LineToBeIndented.Text.Lines.GetLineFromPosition(token.SpanStart)) End If ' check alignment token indentation Dim alignmentTokenIndentation = Finder.FromAlignTokensOperations(Tree, token) If alignmentTokenIndentation.HasValue Then Return IndentFromStartOfLine(alignmentTokenIndentation.Value) End If Return Nothing End Function Private Function GetIndentationFromTokenLineAfterLineContinuation(token As SyntaxToken, trivia As SyntaxTrivia) As IndentationResult Dim sourceText = LineToBeIndented.Text Dim position = LineToBeIndented.Start position = GetCurrentPositionNotBelongToEndOfFileToken(position) Dim currentTokenLine = sourceText.Lines.GetLineFromPosition(token.SpanStart) ' error case where the line continuation belongs to a meaningless token such as empty token for skipped text If token.Kind = SyntaxKind.EmptyToken Then Dim baseLine = sourceText.Lines.GetLineFromPosition(trivia.SpanStart) Return GetIndentationOfLine(baseLine) End If Dim xmlEmbeddedExpression = token.GetAncestor(Of XmlEmbeddedExpressionSyntax)() If xmlEmbeddedExpression IsNot Nothing Then Dim firstExpressionLine = sourceText.Lines.GetLineFromPosition(xmlEmbeddedExpression.GetFirstToken(includeZeroWidth:=True).SpanStart) Return GetIndentationFromTwoLines(firstExpressionLine, currentTokenLine, token, position) End If If FormattingHelpers.IsGreaterThanInAttribute(token) Then Dim attribute = token.GetAncestor(Of AttributeListSyntax)() Dim baseLine = sourceText.Lines.GetLineFromPosition(attribute.GetFirstToken(includeZeroWidth:=True).SpanStart) Return GetIndentationOfLine(baseLine) End If ' if position is between "," and next token, consider the position to be belonged to the list that ' owns the "," If IsCommaInParameters(token) AndAlso (token.Span.End <= position AndAlso position <= token.GetNextToken().SpanStart) Then Return GetIndentationOfCurrentPosition(token, token.SpanStart) End If Dim statement = token.GetAncestor(Of StatementSyntax)() ' this can happen if only token in the file is End Of File Token If statement Is Nothing Then If trivia.Kind <> SyntaxKind.None Then Dim triviaLine = sourceText.Lines.GetLineFromPosition(trivia.SpanStart) Return GetIndentationOfLine(triviaLine, Me.OptionSet.GetOption(FormattingOptions.IndentationSize, token.Language)) End If ' no base line to use to calculate the indentation Return IndentFromStartOfLine(0) End If ' find line where first token of statement is starting on Dim firstTokenLine = sourceText.Lines.GetLineFromPosition(statement.GetFirstToken(includeZeroWidth:=True).SpanStart) Return GetIndentationFromTwoLines(firstTokenLine, currentTokenLine, token, position) End Function Private Function IsCommaInParameters(token As SyntaxToken) As Boolean Return token.Kind = SyntaxKind.CommaToken AndAlso (TypeOf token.Parent Is ParameterListSyntax OrElse TypeOf token.Parent Is ArgumentListSyntax OrElse TypeOf token.Parent Is TypeParameterListSyntax) End Function Private Function GetIndentationFromTwoLines(firstLine As TextLine, secondLine As TextLine, token As SyntaxToken, position As Integer) As IndentationResult If firstLine.LineNumber = secondLine.LineNumber Then ' things are on same line, put the indentation size Return GetIndentationOfCurrentPosition(token, position, Me.OptionSet.GetOption(FormattingOptions.IndentationSize, token.Language)) End If ' multiline Return GetIndentationOfLine(secondLine) End Function Private Function HasLinesBetween(lineNumber1 As Integer, lineNumber2 As Integer) As Boolean Return lineNumber1 + 1 < lineNumber2 End Function End Class End Class End Namespace