有没有一种方法可以在文本文件中自动缩进VB.NET代码?

8
我希望能够正确地缩进一个包含在文本文件中的VB.NET代码。有没有什么方法可以实现这个目标?
例如: 从以下代码开始:
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

完成这个:

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

到目前为止,我所搜寻的一切都指向了IndentedTextWriter类,但我找到的唯一示例是手动缩进行,如下所示:.NET Console TextWriter that Understands Indent/Unindent/IndentLevel

额外加分项:如果可能的话,我也想添加正确的间距:

例如:Dim i As String="Hello"+"GoodBye" -> Dim i As String = "Hello" + "GoodBye"


哦,是的。编写一个理解vb.net语法的程序完全是另一回事了。以前很容易,但现在行继续符是可选的,这可能会让你头痛不已。 - Hans Passant
你是从VB项目中复制文本还是直接在记事本或.txt文件中编写的? - Kashish Arora
@KashishArora - 我正在为我们产品的客户提供编写vb.net '脚本'的能力,他们将使用记事本对其进行编辑。我想要为他们缩进此文件的能力。 - Matt Wilko
@MattWilko:你是在寻找一个能够完成大部分工作的软件包(听起来是这样),还是想自己动手做呢? - Ira Baxter
@IraBaxter - 我希望框架中有一些内置功能可以让我这样做,但似乎没有,所以选择要么是自己编写代码,要么就是使用第三方程序集来完成。 - Matt Wilko
显示剩余2条评论
5个回答

3

如果你正在使用Visual Studio(我目前在看VS 2010,我不知道早期版本做了什么),那么你可以进入“编辑”->“高级”->“格式化文档”,它会为你处理缩进和间距。

请注意,这适用于Visual Studio理解的任何类型的文档。我经常使用这个技巧将XML文档格式化为可读性更好的形式。


谢谢,但我想在没有安装VS的机器上完成这个任务。 - Matt Wilko
1
在没有使用IDE的情况下格式化VB代码是一个相当大的限制。正如@HansPassant上面提到的,编写自己的VB解析器将是一项非常困难的任务。你并没有真正提到为什么要这样做,但至少值得考虑是否在相关计算机上安装免费的入门级Visual Studio版本,即使只是用来格式化你的文件。 - shooley

1

如果您可以接受使用预发布软件,您可以使用Roslyn

Dim parsed = Syntax.ParseCompilationUnit(text)
Dim normalized = parsed.NormalizeWhitespace()
Console.WriteLine(normalized)

0

我决定尝试自己编写。有一些边缘情况它不能百分之百地工作,但它相当可靠:

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

使用方法:

将一些代码放入文本框(TextBox1)中,然后像这样调用缩进器:

Dim id As New VBIndenter
id.Indent(TextBox1)

-1
  1. 这适用于C语法。稍微修改一下,就可以用于文件了。这个提供实时缩进
  2. 概念相同,只需将其修改为(每当找到文件的chr(13)或换行符)而不是(e.keycode=13),并将您的(文件阅读器的当前索引I)而不是TextBox1.SelectionStart。
  3. 进行轻微更改以适应您的文件需求。我没有重新编写,因为任何想要制作IDE的人都会发现这很方便。使用代码转换器转换C#代码。

这是一小段代码:

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

-3
一种方法是构建解析器和漂亮打印器来实现这个目标。解析器读取源代码并构建一个捕获程序结构本质的AST。漂亮打印器接受树形结构,并基于结构重新生成输出;因此,它很容易得到结构化的输出。一个关键提示是,对于每个语言结构层次(类、方法、块、循环、条件语句),漂亮打印器可以缩进漂亮打印文本以提供良好的缩进结构。
解析和漂亮打印都是相当复杂的主题。不必在此重复所有内容,您可以查看我的SO答案,了解如何解析,以及如何构建AST的后续讨论。漂亮打印并不是很出名,但是我的这个SO答案提供了一个相当完整的描述如何实现它。
然后你需要确定VB.net的实际语法的复杂性。这需要从参考文档中提取大量的工作...而且它并不完全正确,所以你需要针对大量的代码验证你的解析器以确信它是正确的。不幸的是,这部分只能靠汗水来完成。

给定一个漂亮打印程序,OP可以简单地启动它作为进程来格式化文件。

如果您做到了这一点,那么您可以格式化VB.net文本。我们的(独立的)VB.net格式化程序(“DMSFormat ...”)执行上述操作以实现漂亮的打印。

给定文件“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

以下内容:

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

产生:

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

这与 OP 在他的示例中所需的完全相同。

您可以轻松地将格式化的程序内容直接写入文件。

您可以给工具一个项目文件,它会一次性格式化您在项目文件中指定的所有文件。

该格式化程序集成了完整的 VB.net 解析器和我们自己的漂亮打印机。它精确地解析源文本(包括奇怪的字符编码)。因为它使用可靠的解析器和漂亮的打印机,所以不会破坏代码。

评估版本适用于几百行代码的文件。这可能正是您所需要的。

我想提供一个链接,但 SO 似乎不喜欢那样做。您可以通过我的个人简介找到它。


这很棒,但你没有提供工具的链接,也没有说明“我们”是谁提到的。 - RubberDuck
这是因为 Stack Overflow 的政策反对此类行为,而且当我发布指向“我们”的工具链接时,我会受到巨大的压力。 “我们”指的就是我们自己的工具,请查看我的个人简介。如果您认为这些限制不合理,请在 Meta 中提出。 - Ira Baxter
1
没有这些信息,这个答案就毫无用处。我不是应该在元站上发表意见的人。你应该去。事实上,你会发现这样的回答是完全可以接受的,因为它与问题直接相关 - RubberDuck
这就是我喜欢 Stack Overflow 的原因。我提供了一个生产可用的答案,却被踩了。 - Ira Baxter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接