.netvb.netindentationcode-formattingcodedom

Is there some way to automatically indent VB.NET code in a text file


I would like to correctly indent some VB.NET code contained within a text file. Is there some way to do this?

e.g. Start with this:

Public Shared Function CanReachPage(page As String) As Boolean
Try
Using client = New WebClient()
Using stream = client.OpenRead(page)
Return True
End Using
End Using
Catch
Return False
End Try
End Function

finish up with this:

Public Shared Function CanReachPage(page As String) As Boolean
    Try
        Using client = New WebClient()
            Using stream = client.OpenRead(page)
                Return True
            End Using
        End Using
    Catch
        Return False
    End Try
End Function

Everything I have searched for has so far led me to the IndentedTextWriter Class but the only examples I have found are to manually indent lines like this: .NET Console TextWriter that Understands Indent/Unindent/IndentLevel

Extra credit: I would also like to add the correct spacing as well if possible:

e.g Dim i As String="Hello"+"GoodBye" -> Dim i As String = "Hello" + "GoodBye"


Solution

  • I decided to have a crack at rolling by own. There are some edge cases that this doesn't work 100% for, but it is pretty reliable:

    Public Class VBIndenter
    
        Private _classIndents As New List(Of Integer)
        Private _moduleIndents As New List(Of Integer)
        Private _subIndents As New List(Of Integer)
        Private _functionIndents As New List(Of Integer)
        Private _propertyIndents As New List(Of Integer)
        Private _structureIndents As New List(Of Integer)
        Private _enumIndents As New List(Of Integer)
        Private _usingIndents As New List(Of Integer)
        Private _withIndents As New List(Of Integer)
        Private _ifIndents As New List(Of Integer)
        Private _tryIndents As New List(Of Integer)
        Private _getIndents As New List(Of Integer)
        Private _setIndents As New List(Of Integer)
        Private _forIndents As New List(Of Integer)
        Private _selectIndents As New List(Of Integer)
        Private _doIndents As New List(Of Integer)
        Private _whileIndents As New List(Of Integer)
    
        Public Property IndentWidth As Integer = 4
        Public Property IndentChar As Char = " "c
    
        Public Sub Indent(txt As TextBox)
    
            Dim lastLabelIndent As Integer = 0
            Dim lastRegionIndent As Integer = 0
            Dim currentIndent As Integer = 0
            Dim inProperty As Boolean = False
            Dim lineText As String
            Dim newLineIndent As Integer
            Dim lines As String() = txt.Lines
    
            For i As Integer = 0 To lines.Count - 1
    
                Dim line = lines(i)
    
                'get the trimmed line without any comments
                lineText = StripComments(line)
    
                'only change the indent on lines that are code
                If lineText.Length > 0 Then
    
                    'special case for regions and labels - they always have zero indent
                    If lineText.StartsWith("#") Then
                        lastRegionIndent = currentIndent
                        currentIndent = 0
                    ElseIf lineText.EndsWith(":") Then
                        lastLabelIndent = currentIndent
                        currentIndent = 0
                    End If
    
                    'if we are in a property and we see something 
                    If (_propertyIndents.Count > 0) Then
                        If Not lineText.StartsWith("End") Then
                            If lineText.StartsWith("Class ") OrElse lineText.Contains(" Class ") Then
                                _propertyIndents.RemoveAt(_propertyIndents.Count - 1)
                                currentIndent -= 1
                            ElseIf lineText.StartsWith("Module ") OrElse lineText.Contains(" Module ") Then
                                _propertyIndents.RemoveAt(_propertyIndents.Count - 1)
                                currentIndent -= 1
                            ElseIf lineText.StartsWith("Sub ") OrElse lineText.Contains(" Sub ") Then
                                _propertyIndents.RemoveAt(_propertyIndents.Count - 1)
                                currentIndent -= 1
                            ElseIf lineText.StartsWith("Function ") OrElse lineText.Contains(" Function ") Then
                                _propertyIndents.RemoveAt(_propertyIndents.Count - 1)
                                currentIndent -= 1
                            ElseIf lineText.StartsWith("Property ") OrElse lineText.Contains(" Property ") Then
                                _propertyIndents.RemoveAt(_propertyIndents.Count - 1)
                                currentIndent -= 1
                            ElseIf lineText.StartsWith("Structure ") OrElse lineText.Contains(" Structure ") Then
                                _propertyIndents.RemoveAt(_propertyIndents.Count - 1)
                                currentIndent -= 1
                            ElseIf lineText.StartsWith("Enum ") OrElse lineText.Contains(" Enum ") Then
                                _propertyIndents.RemoveAt(_propertyIndents.Count - 1)
                                currentIndent -= 1
                            End If
                        Else
                            If lineText = "End Class" Then
                                _propertyIndents.RemoveAt(_propertyIndents.Count - 1)
                            End If
                        End If
                    End If
    
                    If lineText = "End Class" Then
                        currentIndent = _classIndents.Item(_classIndents.Count - 1)
                        _classIndents.RemoveAt(_classIndents.Count - 1)
                    ElseIf lineText = "End Module" Then
                        currentIndent = _moduleIndents.Item(_moduleIndents.Count - 1)
                        _moduleIndents.RemoveAt(_moduleIndents.Count - 1)
                    ElseIf lineText = "End Sub" Then
                        currentIndent = _subIndents.Item(_subIndents.Count - 1)
                        _subIndents.RemoveAt(_subIndents.Count - 1)
                    ElseIf lineText = "End Function" Then
                        currentIndent = _functionIndents.Item(_functionIndents.Count - 1)
                        _functionIndents.RemoveAt(_functionIndents.Count - 1)
                    ElseIf lineText = "End Property" Then
                        currentIndent = _propertyIndents.Item(_propertyIndents.Count - 1)
                        _propertyIndents.RemoveAt(_propertyIndents.Count - 1)
                    ElseIf lineText = "End Try" Then
                        currentIndent = _tryIndents.Item(_tryIndents.Count - 1)
                        _tryIndents.RemoveAt(_tryIndents.Count - 1)
                    ElseIf lineText = "End With" Then
                        currentIndent = _withIndents.Item(_withIndents.Count - 1)
                        _withIndents.RemoveAt(_withIndents.Count - 1)
                    ElseIf lineText = "End Get" Then
                        currentIndent = _getIndents.Item(_getIndents.Count - 1)
                        _getIndents.RemoveAt(_getIndents.Count - 1)
                    ElseIf lineText = "End Set" Then
                        currentIndent = _setIndents.Item(_setIndents.Count - 1)
                        _setIndents.RemoveAt(_setIndents.Count - 1)
                    ElseIf lineText = "End If" Then
                        currentIndent = _ifIndents.Item(_ifIndents.Count - 1)
                        _ifIndents.RemoveAt(_ifIndents.Count - 1)
                    ElseIf lineText = "End Using" Then
                        currentIndent = _usingIndents.Item(_usingIndents.Count - 1)
                        _usingIndents.RemoveAt(_usingIndents.Count - 1)
                    ElseIf lineText = "End Structure" Then
                        currentIndent = _structureIndents.Item(_structureIndents.Count - 1)
                        _structureIndents.RemoveAt(_structureIndents.Count - 1)
                    ElseIf lineText = "End Select" Then
                        currentIndent = _selectIndents.Item(_selectIndents.Count - 1)
                        _selectIndents.RemoveAt(_selectIndents.Count - 1)
                    ElseIf lineText = "End Enum" Then
                        currentIndent = _enumIndents.Item(_enumIndents.Count - 1)
                        _enumIndents.RemoveAt(_enumIndents.Count - 1)
                    ElseIf lineText = "End While" OrElse lineText = "Wend" Then
                        currentIndent = _whileIndents.Item(_whileIndents.Count - 1)
                        _whileIndents.RemoveAt(_whileIndents.Count - 1)
                    ElseIf lineText = "Next" OrElse lineText.StartsWith("Next ") Then
                        currentIndent = _forIndents.Item(_forIndents.Count - 1)
                        _forIndents.RemoveAt(_forIndents.Count - 1)
                    ElseIf lineText = "Loop" OrElse lineText.StartsWith("Loop ") Then
                        currentIndent = _doIndents.Item(_doIndents.Count - 1)
                        _doIndents.RemoveAt(_doIndents.Count - 1)
                    ElseIf lineText.StartsWith("Else") Then
                        currentIndent = _ifIndents.Item(_ifIndents.Count - 1)
                    ElseIf lineText.StartsWith("Catch") Then
                        currentIndent = _tryIndents.Item(_tryIndents.Count - 1)
                    ElseIf lineText.StartsWith("Case") Then
                        currentIndent = _selectIndents.Item(_selectIndents.Count - 1) + 1
                    ElseIf lineText = "Finally" Then
                        currentIndent = _tryIndents.Item(_tryIndents.Count - 1)
                    End If
    
                End If
    
                'find the current indent
                newLineIndent = currentIndent * Me.IndentWidth
                'change the indent of the current line 
                line = New String(IndentChar, newLineIndent) & line.TrimStart
                lines(i) = line
    
                If lineText.Length > 0 Then
                    If lineText.StartsWith("#") Then
                        currentIndent = lastRegionIndent
                    ElseIf lineText.EndsWith(":") Then
                        currentIndent = lastLabelIndent
                    End If
    
                    If Not lineText.StartsWith("End") Then
                        If (lineText.StartsWith("Class ") OrElse lineText.Contains(" Class ")) Then
                            _classIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf (lineText.StartsWith("Module ") OrElse lineText.Contains(" Module ")) Then
                            _moduleIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf (lineText.StartsWith("Sub ") OrElse lineText.Contains(" Sub ")) Then
                            _subIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf (lineText.StartsWith("Function ") OrElse lineText.Contains(" Function ")) Then
                            _functionIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf (lineText.StartsWith("Property ") OrElse lineText.Contains(" Property ")) Then
                            _propertyIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf (lineText.StartsWith("Structure ") OrElse lineText.Contains(" Structure ")) Then
                            _structureIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf (lineText.StartsWith("Enum ") OrElse lineText.Contains(" Enum ")) Then
                            _enumIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText.Contains("Using ") Then
                            _usingIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText.StartsWith("Select Case") Then
                            _selectIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText = "Try" Then
                            _tryIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText = "Get" Then
                            _getIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText.StartsWith("Set") AndAlso Not lineText.Contains("=") Then
                            _setIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText.StartsWith("With") Then
                            _withIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText.StartsWith("If") AndAlso lineText.EndsWith("Then") Then
                            _ifIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText.StartsWith("For") Then
                            _forIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText.StartsWith("While") Then
                            _whileIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText.StartsWith("Do") Then
                            _doIndents.Add(currentIndent)
                            currentIndent += 1
                        ElseIf lineText.StartsWith("Case") Then
                            currentIndent += 1
                        ElseIf lineText.StartsWith("Else") Then
                            currentIndent = _ifIndents.Item(_ifIndents.Count - 1) + 1
                        ElseIf lineText.StartsWith("Catch") Then
                            currentIndent = _tryIndents.Item(_tryIndents.Count - 1) + 1
                        ElseIf lineText = "Finally" Then
                            currentIndent = _tryIndents.Item(_tryIndents.Count - 1) + 1
                        End If
                    End If
                End If
            Next
            'update the textbox
            txt.Lines = lines
        End Sub
    
        Private Function StripComments(ByVal code As String) As String
            If code.IndexOf("'"c) >= 0 Then
                code = code.Substring(0, code.IndexOf("'"c))
            End If
            Return code.Trim
        End Function
    End Class
    

    Usage:

    Put some code into a TextBox (TextBox1), then call the indenter like this:

    Dim id As New VBIndenter
    id.Indent(TextBox1)