File: EditAndContinue\BreakpointSpans.vb
Web Access
Project: src\src\roslyn\src\Features\VisualBasic\Portable\Microsoft.CodeAnalysis.VisualBasic.Features.vbproj (Microsoft.CodeAnalysis.VisualBasic.Features)
' 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.Runtime.InteropServices
Imports Microsoft.CodeAnalysis.VisualBasic
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Imports Microsoft.CodeAnalysis.Text
Imports System.Threading

Namespace Microsoft.CodeAnalysis.VisualBasic.EditAndContinue
    Friend Module BreakpointSpans
        Friend Function TryGetBreakpointSpan(tree As SyntaxTree, position As Integer, cancellationToken As CancellationToken, <Out> ByRef breakpointSpan As TextSpan) As Boolean
            Dim source = tree.GetText(cancellationToken)

            ' If the line is entirely whitespace, then don't set any breakpoint there.
            Dim line = source.Lines.GetLineFromPosition(position)
            If IsBlank(line) Then
                breakpointSpan = Nothing
                Return False
            End If

            ' If the user is asking for breakpoint in an inactive region, then just create a line
            ' breakpoint there.
            If tree.IsInInactiveRegion(position, cancellationToken) Then
                breakpointSpan = Nothing
                Return True
            End If

            Dim root = tree.GetRoot(cancellationToken)
            Return TryGetClosestBreakpointSpan(root, position, minLength:=0, breakpointSpan)
        End Function

        Private Function IsBlank(line As TextLine) As Boolean
            Dim text = line.ToString()

            For i = 0 To text.Length - 1
                If Not SyntaxFacts.IsWhitespace(text(i)) Then
                    Return False
                End If
            Next

            Return True
        End Function

        ''' <summary>
        ''' Given a syntax token determines a text span delimited by the closest applicable sequence points 
        ''' encompassing the token.
        ''' </summary>
        ''' <remarks>
        ''' If the span exists it Is possible To place a breakpoint at the given position.
        ''' </remarks>
        ''' <param name="minLength">
        ''' In case there are multiple breakpoint spans starting at the given <paramref name="position"/>,
        ''' <paramref name="minLength"/> can be used to disambiguate between them. 
        ''' The inner-most available span whose length is at least <paramref name="minLength"/> is returned.
        ''' </param>
        Public Function TryGetClosestBreakpointSpan(root As SyntaxNode, position As Integer, minLength As Integer, <Out> ByRef span As TextSpan) As Boolean
            Dim node = root.FindToken(position).Parent

            Dim candidate As TextSpan? = Nothing
            While node IsNot Nothing
                Dim breakpointSpan = TryCreateSpanForNode(node, position)
                If breakpointSpan.HasValue Then
                    If breakpointSpan.Value = New TextSpan() Then
                        Exit While
                    End If

                    ' the new breakpoint span doesn't alight with the previously found breakpoint span, return the previous one:
                    If candidate.HasValue AndAlso breakpointSpan.Value.Start <> candidate.Value.Start Then
                        span = candidate.Value
                        Return True
                    End If

                    ' The span length meets the requirement:
                    If breakpointSpan.Value.Length >= minLength Then
                        span = breakpointSpan.Value
                        Return True
                    End If

                    candidate = breakpointSpan
                End If

                node = node.Parent
            End While

            span = candidate.GetValueOrDefault()
            Return candidate.HasValue
        End Function

        Private Function CreateSpan(node As SyntaxNode) As TextSpan
            Return TextSpan.FromBounds(node.SpanStart, node.Span.End)
        End Function

        Private Function TryCreateSpan(Of TNode As SyntaxNode)(list As SeparatedSyntaxList(Of TNode)) As TextSpan?
            If list.Count = 0 Then
                Return Nothing
            End If

            Return TextSpan.FromBounds(list.First.SpanStart, list.Last.Span.End)
        End Function

        Private Function TryCreateSpanForNode(node As SyntaxNode, position As Integer) As TextSpan?
            Select Case node.Kind
                Case SyntaxKind.VariableDeclarator,
                     SyntaxKind.ModifiedIdentifier
                    ' Handled by parent field or local variable declaration.
                    Return Nothing

                Case SyntaxKind.FieldDeclaration
                    Dim fieldDeclaration = DirectCast(node, FieldDeclarationSyntax)
                    Return TryCreateSpanForVariableDeclaration(fieldDeclaration.Modifiers, fieldDeclaration.Declarators, position)

                Case SyntaxKind.LocalDeclarationStatement
                    Dim localDeclaration = DirectCast(node, LocalDeclarationStatementSyntax)
                    Return TryCreateSpanForVariableDeclaration(localDeclaration.Modifiers, localDeclaration.Declarators, position)

                Case SyntaxKind.PropertyStatement
                    Return TryCreateSpanForPropertyStatement(DirectCast(node, PropertyStatementSyntax))

                ' Statements that are not executable yet marked with sequence points
                Case SyntaxKind.IfStatement,
                     SyntaxKind.ElseIfStatement,
                     SyntaxKind.ElseStatement,
                     SyntaxKind.EndIfStatement,
                     SyntaxKind.UsingStatement,
                     SyntaxKind.EndUsingStatement,
                     SyntaxKind.SyncLockStatement,
                     SyntaxKind.EndSyncLockStatement,
                     SyntaxKind.WithStatement,
                     SyntaxKind.EndWithStatement,
                     SyntaxKind.SimpleDoStatement, SyntaxKind.DoWhileStatement, SyntaxKind.DoUntilStatement,
                     SyntaxKind.SimpleLoopStatement, SyntaxKind.LoopWhileStatement, SyntaxKind.LoopUntilStatement,
                     SyntaxKind.WhileStatement,
                     SyntaxKind.EndWhileStatement,
                     SyntaxKind.ForStatement,
                     SyntaxKind.ForEachStatement,
                     SyntaxKind.NextStatement,
                     SyntaxKind.SelectStatement,
                     SyntaxKind.CaseStatement,
                     SyntaxKind.CaseElseStatement,
                     SyntaxKind.EndSelectStatement,
                     SyntaxKind.TryStatement,
                     SyntaxKind.CatchStatement,
                     SyntaxKind.FinallyStatement,
                     SyntaxKind.EndTryStatement,
                     SyntaxKind.EndSubStatement,
                     SyntaxKind.EndFunctionStatement,
                     SyntaxKind.EndOperatorStatement,
                     SyntaxKind.EndGetStatement,
                     SyntaxKind.EndSetStatement,
                     SyntaxKind.EndAddHandlerStatement,
                     SyntaxKind.EndRemoveHandlerStatement,
                     SyntaxKind.EndRaiseEventStatement,
                     SyntaxKind.FunctionLambdaHeader,
                     SyntaxKind.SubLambdaHeader
                    Return CreateSpan(node)

                Case SyntaxKind.SubStatement,
                     SyntaxKind.SubNewStatement,
                     SyntaxKind.FunctionStatement,
                     SyntaxKind.OperatorStatement,
                     SyntaxKind.GetAccessorStatement,
                     SyntaxKind.SetAccessorStatement,
                     SyntaxKind.AddHandlerAccessorStatement,
                     SyntaxKind.RemoveHandlerAccessorStatement,
                     SyntaxKind.RaiseEventAccessorStatement
                    Return CreateSpanForMethodBase(DirectCast(node, MethodBaseSyntax))

                Case SyntaxKind.SingleLineIfStatement
                    Dim asSingleLine = DirectCast(node, SingleLineIfStatementSyntax)

                    If position >= asSingleLine.IfKeyword.SpanStart AndAlso position < asSingleLine.ThenKeyword.Span.End Then
                        Return TextSpan.FromBounds(asSingleLine.IfKeyword.SpanStart, asSingleLine.ThenKeyword.Span.End)
                    Else
                        Return CreateSpan(node)
                    End If

                Case SyntaxKind.SingleLineElseClause
                    Dim asSingleLineElse = DirectCast(node, SingleLineElseClauseSyntax)

                    Return asSingleLineElse.ElseKeyword.Span

                Case SyntaxKind.FunctionAggregation
                    Return TryCreateSpanForFunctionAggregation(DirectCast(node, FunctionAggregationSyntax))

                Case SyntaxKind.SingleLineFunctionLambdaExpression,
                     SyntaxKind.SingleLineSubLambdaExpression
                    Return CreateSpan(node)

                Case SyntaxKind.SelectClause
                    Return TryCreateSpanForSelectClause(DirectCast(node, SelectClauseSyntax))

                Case SyntaxKind.WhereClause
                    Return TryCreateSpanForWhereClause(DirectCast(node, WhereClauseSyntax))

                Case SyntaxKind.CollectionRangeVariable
                    Return TryCreateSpanForCollectionRangeVariable(DirectCast(node, CollectionRangeVariableSyntax))

                Case SyntaxKind.LetClause
                    Return TryCreateSpanForLetClause(DirectCast(node, LetClauseSyntax), position)

                Case SyntaxKind.GroupByClause
                    Return TryCreateSpanForGroupByClause(DirectCast(node, GroupByClauseSyntax), position)

                Case SyntaxKind.SkipWhileClause,
                     SyntaxKind.TakeWhileClause
                    Return TryCreateSpanForPartitionWhileClauseSyntax(DirectCast(node, PartitionWhileClauseSyntax))

                Case SyntaxKind.AscendingOrdering,
                     SyntaxKind.DescendingOrdering
                    Return TryCreateSpanForOrderingSyntax(DirectCast(node, OrderingSyntax))

                Case SyntaxKind.OrderByClause
                    Return TryCreateSpanForOrderByClause(DirectCast(node, OrderByClauseSyntax), position)

                Case SyntaxKind.FromClause
                    Return TryCreateSpanForFromClause(DirectCast(node, FromClauseSyntax), position)

                Case Else
                    Dim executableStatement = TryCast(node, ExecutableStatementSyntax)
                    If executableStatement IsNot Nothing Then
                        Return CreateSpan(node)
                    End If

                    Dim expression = TryCast(node, ExpressionSyntax)
                    If expression IsNot Nothing Then
                        Return TryCreateSpanForExpression(expression)
                    End If

                    Return Nothing
            End Select
        End Function

        Private Function CreateSpanForMethodBase(methodBase As MethodBaseSyntax) As TextSpan
            If methodBase.Modifiers.Count = 0 Then
                Return TextSpan.FromBounds(methodBase.DeclarationKeyword.SpanStart, methodBase.Span.End)
            End If

            Return TextSpan.FromBounds(methodBase.Modifiers.First().SpanStart, methodBase.Span.End)
        End Function

        Private Function TryCreateSpanForPropertyStatement(node As PropertyStatementSyntax) As TextSpan?
            If node.Parent.IsKind(SyntaxKind.PropertyBlock) Then
                ' not an auto-property:
                Return Nothing
            End If

            If node.Initializer IsNot Nothing Then
                Return TextSpan.FromBounds(node.Identifier.Span.Start, node.Initializer.Span.End)
            End If

            If node.AsClause IsNot Nothing AndAlso node.AsClause.IsKind(SyntaxKind.AsNewClause) Then
                Return TextSpan.FromBounds(node.Identifier.Span.Start, node.AsClause.Span.End)
            End If

            Return Nothing
        End Function

        Private Function TryCreateSpanForVariableDeclaration(modifiers As SyntaxTokenList, declarators As SeparatedSyntaxList(Of VariableDeclaratorSyntax), position As Integer) As TextSpan?
            If declarators.Count = 0 Then
                Return Nothing
            End If

            If modifiers.Any(SyntaxKind.ConstKeyword) Then
                Return New TextSpan()
            End If

            Dim name = FindClosestNameWithInitializer(declarators, position)
            If name Is Nothing Then
                Return New TextSpan()
            End If

            If name.ArrayBounds IsNot Nothing OrElse DirectCast(name.Parent, VariableDeclaratorSyntax).Names.Count > 1 Then
                Return CreateSpan(name)
            Else
                Return CreateSpan(name.Parent)
            End If
        End Function

        Private Function FindClosestNameWithInitializer(declarators As SeparatedSyntaxList(Of VariableDeclaratorSyntax), position As Integer) As ModifiedIdentifierSyntax
            Return FindClosestNode(declarators, position,
                Function(declarator)
                    If declarator.HasInitializer Then
                        Return declarator.Names(GetItemIndexByPosition(declarator.Names, position))
                    End If

                    Return FindClosestNode(declarator.Names, position, Function(idf)
                                                                           Return If(idf.ArrayBounds IsNot Nothing, idf, Nothing)
                                                                       End Function)
                End Function)
        End Function

        Private Function FindClosestNode(Of TListNode As SyntaxNode, TResult As SyntaxNode)(nodes As SeparatedSyntaxList(Of TListNode), position As Integer, predicate As Func(Of TListNode, TResult)) As TResult
            Dim d = GetItemIndexByPosition(nodes, position)

            Dim i = 0
            Do
                Dim left = d - i
                Dim right = d + i

                If left < 0 AndAlso right >= nodes.Count Then
                    Return Nothing
                End If

                If left >= 0 Then
                    Dim result = predicate(nodes(left))
                    If result IsNot Nothing Then
                        Return result
                    End If
                End If

                If right < nodes.Count Then
                    Dim result = predicate(nodes(right))
                    If result IsNot Nothing Then
                        Return result
                    End If
                End If

                i += 1
            Loop
        End Function

        Private Function GetItemIndexByPosition(Of TNode As SyntaxNode)(list As SeparatedSyntaxList(Of TNode), position As Integer) As Integer
            For i = list.SeparatorCount - 1 To 0 Step -1
                If position > list.GetSeparator(i).SpanStart Then
                    Return i + 1
                End If
            Next

            Return 0
        End Function

        Private Function TryCreateSpanForFromClause(fromClause As FromClauseSyntax, position As Integer) As TextSpan?
            Dim query = DirectCast(fromClause.Parent, QueryExpressionSyntax)

            ' If it's not the first from clause, then you can set the breakpoint on the first
            ' variable.
            If query.Clauses.First() IsNot fromClause AndAlso fromClause.Variables.Any() Then
                Return TryCreateSpanForNode(fromClause.Variables.First(), position)
            End If

            ' If it is the first from clause, you can only set the breakpoint on the second or
            ' higher variable.
            If query.Clauses.First() Is fromClause AndAlso fromClause.Variables.Count > 1 Then
                Return TryCreateSpanForNode(fromClause.Variables(1), position)
            End If

            Return Nothing
        End Function

        Private Function TryCreateSpanForFunctionAggregation(functionAggregation As FunctionAggregationSyntax) As TextSpan?
            If functionAggregation.Argument IsNot Nothing Then
                Return CreateSpan(functionAggregation.Argument)
            End If

            Return Nothing
        End Function

        Private Function TryCreateSpanForOrderByClause(orderByClause As OrderByClauseSyntax, position As Integer) As TextSpan?
            If orderByClause.Orderings.Any() Then
                Return TryCreateSpanForNode(orderByClause.Orderings.First(), position)
            End If

            Return Nothing
        End Function

        Private Function TryCreateSpanForOrderingSyntax(orderingSyntax As OrderingSyntax) As TextSpan?
            Return CreateSpan(orderingSyntax.Expression)
        End Function

        Private Function TryCreateSpanForPartitionWhileClauseSyntax(partitionWhileClause As PartitionWhileClauseSyntax) As TextSpan?
            Return CreateSpan(partitionWhileClause.Condition)
        End Function

        Private Function TryCreateSpanForCollectionRangeVariable(collectionRangeVariable As CollectionRangeVariableSyntax) As TextSpan?
            If collectionRangeVariable.Parent.Kind = SyntaxKind.FromClause Then
                Dim fromClause = DirectCast(collectionRangeVariable.Parent, FromClauseSyntax)
                Dim query = DirectCast(fromClause.Parent, QueryExpressionSyntax)

                ' We can break on this expression if we're not the first clause in a
                ' query, or if the range variable it not the first range variable in the
                ' list.
                If query.Clauses.First() IsNot fromClause OrElse fromClause.Variables.IndexOf(collectionRangeVariable) <> 0 Then
                    Return CreateSpan(collectionRangeVariable.Expression)
                End If
            End If

            Return Nothing
        End Function

        Private Function TryCreateSpanForWhereClause(clause As WhereClauseSyntax) As TextSpan?
            Return CreateSpan(clause.Condition)
        End Function

        Private Function TryCreateSpanForGroupByClause(clause As GroupByClauseSyntax, position As Integer) As TextSpan?
            If position < clause.ByKeyword.SpanStart Then
                If clause.Items.Count = 1 Then
                    Return CreateSpan(clause.Items.Single.Expression)
                End If

                Return TryCreateSpan(clause.Items)
            End If

            If clause.Keys.Count = 0 Then
                Return Nothing
            End If

            If position >= clause.Keys.First.SpanStart AndAlso position < clause.IntoKeyword.SpanStart Then
                If clause.Keys.Count = 1 Then
                    Return CreateSpan(clause.Keys.Single.Expression)
                End If

                Return TryCreateSpan(clause.Keys)
            End If

            Return TextSpan.FromBounds(clause.Keys.First.SpanStart, clause.Span.End)
        End Function

        Private Function TryCreateSpanForSelectClause(clause As SelectClauseSyntax) As TextSpan?
            If clause.Variables.Count = 1 Then
                Return CreateSpan(clause.Variables.Single.Expression)
            End If

            Return TryCreateSpan(clause.Variables)
        End Function

        Private Function TryCreateSpanForLetClause(clause As LetClauseSyntax, position As Integer) As TextSpan?
            Return clause.Variables(GetItemIndexByPosition(clause.Variables, position)).Expression.Span
        End Function

        Private Function TryCreateSpanForExpression(expression As ExpressionSyntax) As TextSpan?
            If IsBreakableExpression(expression) Then
                Return CreateSpan(expression)
            End If

            Return Nothing
        End Function

        Private Function IsBreakableExpression(expression As ExpressionSyntax) As Boolean
            If expression Is Nothing OrElse expression.Parent Is Nothing Then
                Return False
            End If

            Select Case expression.Parent.Kind
                Case SyntaxKind.JoinCondition
                    Dim joinCondition = DirectCast(expression.Parent, JoinConditionSyntax)
                    Return expression Is joinCondition.Left OrElse expression Is joinCondition.Right

                Case SyntaxKind.SingleLineFunctionLambdaExpression
                    Dim lambda = DirectCast(expression.Parent, SingleLineLambdaExpressionSyntax)
                    Return expression Is lambda.Body
            End Select

            Return False
        End Function
    End Module
End Namespace