Question

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"

Was it helpful?

Solution 3

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)

OTHER TIPS

If you're using Visual Studio (I'm looking at VS 2010 at the moment; I don't know offhand what earlier versions did) then you can go to the Edit->Advanced->Format Document and it should take care of the indentation and spacing for you.

Note that this works for any type of document that Visual Studio understands. I regularly use this trick to format XML documents into something legible.

If you're okay with using pre-release software, you could use Roslyn:

Dim parsed = Syntax.ParseCompilationUnit(text)
Dim normalized = parsed.NormalizeWhitespace()
Console.WriteLine(normalized)
  1. This works for C Syntax. Slightly modify it to use it with a file. This one provides real-time indent while typing.
  2. Concept is same just modify it with (whenever it finds a chr(13) or newline character) of file instead of (e.keycode=13) and your (current index I of file reader) instead of TextBox1.SelectionStart.
  3. Make slight changes that would suit your file needs. I am not re-writing as anyone looking to make IDE would find this handy. Use code converter for the C# code.

Here's the little code:

Private Sub TextBox1_KeyUp    
If e.KeyCode = 13 Then
   Dim k = TextBox1.Text.Substring(0, TextBox1.SelectionStart)
   Dim a = k.Split("{").Length - 1, b = k.Split("}").Length - 1
   If (a - b) > -1 Then SendKeys.Send(New String(" ", (a - b) * 2))
End If
End Sub

One way to do this is to build a parser and a prettyprinter. The parser reads the source and builds an AST capturing the essence of the program structure. The prettyprinter takes the tree, and regenerates output based on the structure; thus it is "easy" to get structured output. As a key hint, for each level of language structure (classes, methods, blocks, loops, conditionals), the prettyprinter can indent the prettyprinted text to give good indentation structure.

Parsing and prettyprinting are both pretty complicated topics. Rather than repeat all that here, you can see my SO answer on how to parse, with follow on discussion on how to build an AST. Prettyprinting is not so well known, but this SO answer of mine provides a pretty complete description of how to do it.

Then you have the complication of determining the actual grammar of VB.net. That requires a lot of work to extract from the reference documentation... and it isn't quite right, so you need to validate your parser against a lot of code to convince yourself it right. This part is unfortunately just sweat.

Given a prettyprinter program, OP can simply launch it as a process to format a file.

If you do all that, then yes you can format VB.net text. Our (standalone) VB.net formatter ("DMSFormat ...") does the above to achieve prettyprinting.

Given the file "vb_example.net":

Module Test
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
End Module

The following:

C:>DMSFormat VisualBasic~VBdotNet  C:\temp\vb_example.net

produces:

VisualBasic~VBdotNet Formatter/Obfuscator Version 1.2.1
Copyright (C) 2010 Semantic Designs, Inc
Powered by DMS (R) Software Reengineering Toolkit
Parsing C:\temp\vb_example.net [encoding ISO-8859-1]


Module Test
  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
End Module

which is identical to what OP wanted in his example.

You can easily direct the formatted program content to a file.

You can give the tool a project file, and it will format as many files as you specify in the project file at once.

The formatter integrates a complete VB.net parser, and our own prettyprinting machinery. It parses the source text precisely (including strange character encodings). Because it uses a reliable parser and prettyprinter, it will not break the code.

The eval version works on files of a few hundred lines of code. It might be just what you need.

I'd provide a link but SO seems to dislike that. You can find this through my bio.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top