' 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. Imports System.Collections.Immutable Imports Microsoft.CodeAnalysis.Formatting Imports Microsoft.CodeAnalysis.Formatting.Rules Imports Microsoft.CodeAnalysis.Indentation Imports Microsoft.CodeAnalysis.LanguageService Imports Microsoft.CodeAnalysis.Text Imports Microsoft.CodeAnalysis.VisualBasic.Formatting Imports Microsoft.CodeAnalysis.VisualBasic.LanguageService Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.Indentation Partial Friend Class VisualBasicIndentationService Protected Overrides ReadOnly Property SyntaxFacts As ISyntaxFacts Get Return VisualBasicSyntaxFacts.Instance End Get End Property Protected Overrides ReadOnly Property HeaderFacts As IHeaderFacts Get Return VisualBasicHeaderFacts.Instance End Get End Property Protected Overrides ReadOnly Property SyntaxFormatting As ISyntaxFormatting Get Return VisualBasicSyntaxFormatting.Instance End Get End Property Protected Overrides Function ShouldUseTokenIndenter(indenter As Indenter, ByRef token As SyntaxToken) As Boolean Return ShouldUseSmartTokenFormatterInsteadOfIndenter( indenter.Rules, indenter.Root, indenter.LineToBeIndented, indenter.Options.FormattingOptions, token) End Function Protected Overrides Function CreateSmartTokenFormatter( root As CompilationUnitSyntax, text As SourceText, lineToBeIndented As TextLine, options As IndentationOptions, baseIndentationRule As AbstractFormattingRule) As ISmartTokenFormatter Dim rules = ImmutableArray.Create(New SpecialFormattingRule(options.IndentStyle), baseIndentationRule). AddRange(VisualBasicSyntaxFormatting.Instance.GetDefaultFormattingRules()) Return New VisualBasicSmartTokenFormatter(options.FormattingOptions, rules, root) End Function Protected Overrides Function GetDesiredIndentationWorker( indenter As Indenter, tokenOpt As SyntaxToken?, triviaOpt As SyntaxTrivia?) As IndentationResult? If triviaOpt.HasValue Then Dim trivia = triviaOpt.Value If trivia.Kind = SyntaxKind.CommentTrivia OrElse trivia.Kind = SyntaxKind.DocumentationCommentTrivia Then ' if the comment is the only thing on a line, then preserve its indentation for the next line. Dim line = indenter.Text.Lines.GetLineFromPosition(trivia.FullSpan.Start) If line.GetFirstNonWhitespacePosition() = trivia.FullSpan.Start Then Return New IndentationResult(trivia.FullSpan.Start, 0) End If End If If trivia.Kind = SyntaxKind.CommentTrivia Then ' Line ends in comment ' Two cases a line ending comment or _ comment If tokenOpt.HasValue Then Dim firstTrivia As SyntaxTrivia = indenter.Tree.GetRoot(indenter.CancellationToken).FindTrivia(tokenOpt.Value.Span.End + 1) ' firstTrivia contains either an _ or a comment, this is the First trivia after the last Token on the line If firstTrivia.Kind = SyntaxKind.LineContinuationTrivia Then Return GetIndentationBasedOnToken(indenter, GetTokenOnLeft(firstTrivia), firstTrivia) Else ' This is we have just a comment Return GetIndentationBasedOnToken(indenter, GetTokenOnLeft(trivia), trivia) End If End If 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(indenter, GetTokenOnLeft(trivia), trivia) End If If trivia.Kind = SyntaxKind.LineContinuationTrivia Then Return GetIndentationBasedOnToken(indenter, GetTokenOnLeft(trivia), trivia) End If End If If tokenOpt.HasValue Then Return GetIndentationBasedOnToken(indenter, tokenOpt.Value) End If Return Nothing End Function Private Shared 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 Shared Function GetIndentationBasedOnToken(indenter As Indenter, token As SyntaxToken, Optional trivia As SyntaxTrivia = Nothing) As IndentationResult Dim sourceText = indenter.LineToBeIndented.Text Dim position = indenter.GetCurrentPositionNotBelongToEndOfFileToken(indenter.LineToBeIndented.Start) ' lines must be blank since we got the token from the first non blank line above current position If HasLinesBetween(indenter.Tree.GetText().Lines.IndexOf(token.Span.End), indenter.LineToBeIndented.LineNumber) Then ' if there are blank lines between, return indentation of the owning statement Return GetIndentationOfCurrentPosition(indenter, token, position) End If Dim indentation = GetIndentationFromOperationService(indenter, 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, indenter.Options.FormattingOptions.IndentationSize) Return indenter.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 indenter.GetIndentationOfToken(token) End If If FormattingHelpers.IsXmlTokenInXmlDeclaration(token) Then Dim xmlDocument = token.GetAncestor(Of XmlDocumentSyntax)() Return indenter.GetIndentationOfToken(xmlDocument.GetFirstToken(includeZeroWidth:=True)) End If ' implicit line continuation case If IsLineContinuable(token, trivia) Then Return GetIndentationFromTokenLineAfterLineContinuation(indenter, token, trivia) End If Return GetIndentationOfCurrentPosition(indenter, token, position) End Function Private Shared Function GetIndentationOfCurrentPosition(indenter As Indenter, token As SyntaxToken, position As Integer) As IndentationResult Return GetIndentationOfCurrentPosition(indenter, token, position, extraSpaces:=0) End Function Private Shared Function GetIndentationOfCurrentPosition(indenter As Indenter, token As SyntaxToken, position As Integer, extraSpaces As Integer) As IndentationResult ' special case for multi-line string Dim containingToken = indenter.Tree.FindTokenOnLeftOfPosition(position, indenter.CancellationToken) If containingToken.IsKind(SyntaxKind.InterpolatedStringTextToken) OrElse containingToken.IsKind(SyntaxKind.InterpolatedStringText) OrElse (containingToken.IsKind(SyntaxKind.CloseBraceToken) AndAlso token.Parent.IsKind(SyntaxKind.Interpolation)) Then Return indenter.IndentFromStartOfLine(0) End If If containingToken.Kind = SyntaxKind.StringLiteralToken AndAlso containingToken.FullSpan.Contains(position) Then Return indenter.IndentFromStartOfLine(0) End If Return indenter.IndentFromStartOfLine(indenter.Finder.GetIndentationOfCurrentPosition(indenter.Tree, token, position, extraSpaces, indenter.CancellationToken)) End Function Private Shared Function IsLineContinuable(lastVisibleTokenOnPreviousLine As SyntaxToken, trivia As SyntaxTrivia) 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 Shared 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 Shared 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 Shared Function GetIndentationFromOperationService(indenter As Indenter, 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 = indenter.Finder.FromIndentBlockOperations(indenter.Tree, token, position, indenter.CancellationToken) If indentation.HasValue Then Return indenter.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 indenter.GetIndentationOfLine(indenter.LineToBeIndented.Text.Lines.GetLineFromPosition(token.SpanStart)) End If ' check alignment token indentation Dim alignmentTokenIndentation = indenter.Finder.FromAlignTokensOperations(indenter.Tree, token) If alignmentTokenIndentation.HasValue Then Return indenter.IndentFromStartOfLine(alignmentTokenIndentation.Value) End If Return Nothing End Function Private Shared Function GetIndentationFromTokenLineAfterLineContinuation(indenter As Indenter, token As SyntaxToken, trivia As SyntaxTrivia) As IndentationResult Dim sourceText = indenter.LineToBeIndented.Text Dim position = indenter.LineToBeIndented.Start position = indenter.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 indenter.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(indenter, 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 indenter.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(indenter, 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 indenter.GetIndentationOfLine(triviaLine, indenter.Options.FormattingOptions.IndentationSize) End If ' no base line to use to calculate the indentation Return indenter.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(indenter, firstTokenLine, currentTokenLine, token, position) End Function Private Shared 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 Shared Function GetIndentationFromTwoLines(indenter As Indenter, 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(indenter, token, position, indenter.Options.FormattingOptions.IndentationSize) End If ' multiline Return indenter.GetIndentationOfLine(secondLine) End Function Private Shared Function HasLinesBetween(lineNumber1 As Integer, lineNumber2 As Integer) As Boolean Return lineNumber1 + 1 < lineNumber2 End Function End Class End Namespace |