Dnspy/dnSpy/Roslyn/dnSpy.Roslyn.VisualBasic.Internal/SmartIndent/VisualBasicIndentationService.Indenter.vb

307 lines
13 KiB
VB.net
Raw Normal View History

2021-09-20 18:20:01 +02:00
' 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