※タイトルにつられた皆さん。多分イメージしてるものと違いますのでガッカリされないようご注意ください。
Twitterで、IfやForの終了構文を自動補完することができないのかなという疑問が投げかけられた。
VBEには文字入力を検知してマクロを走らせるような機能は無いので、たとえば If ~ Thenと書いてEnterを押すと自動でEnd Ifが入るような仕組みは作れない。
どうしてもやろうと思ったら外部のスニペットソフトを作ってグローバルキーフックという技術もあるけど、大掛かりになるし安定動作するかも分からない。
そこで方向性を変えて、Pythonっぽく終了構文なしでインデントで表されたコードを、正規のVBAコードに変換するようなマクロを書いてみた。
ただし思考実験みたいなものなので、考慮漏れしてるケースも多数あるだろうしまともにテストしていない。
それに、既に終了構文があっても追記してしまうという重大な欠陥がある。
従って、これはただのアイデア紹介のサンプルコードであり、間違ってもこのまま実務で使おうなどと考えないこと。
実行イメージ
マクロ実行前のコードがこちらだとすると、
Sub fnFizzBuzz() Dim ret, i For i = 1 To 100 Select Case 0 Case i Mod 15 ret = "FizzBuzz" Case i Mod 3 ret = "Fizz" Case i Mod 5 ret = "Buzz" Case Else ret = CStr(i) fnFizzBuzz = ret End Sub
マクロによりEnd SelectとNextが補完されたのがこちら。
Sub fnFizzBuzz() Dim ret, i For i = 1 To 100 Select Case 0 Case i Mod 15 ret = "FizzBuzz" Case i Mod 3 ret = "Fizz" Case i Mod 5 ret = "Buzz" Case Else ret = CStr(i) End Select Next fnFizzBuzz = ret End Sub
マクロでVBプロジェクトに直接アクセスする手もあるが、今回は単純にクリップボードにコードをコピーした状態でマクロを実行するとイミディエイトウィンドウに出力される仕様とした。
まず必要なのは、このブログではおなじみのStackクラス。
クラスモジュールを挿入してオブジェクト名をStackとし、以下のコードを貼り付け。
Private Items() As Variant Property Get Count() As Integer Count = UBound(Items) End Property Property Get Top() As Variant Top = Items(UBound(Items)) End Property Public Function Pop() As Variant If UBound(Items) > 0 Then Pop = Items(UBound(Items)) ReDim Preserve Items(UBound(Items) - 1) Else Pop = Empty End If End Function Public Sub Push(ByRef x As Variant) ReDim Preserve Items(UBound(Items) + 1) Items(UBound(Items)) = x End Sub Private Sub Class_Initialize() ReDim Items(0) End Sub
そして以下の2点を参照設定
Microsoft VBScript Regular Expressions 5.5
Microsoft Forms 2.0 Object Library
Formsの方はユーザーフォームを挿入してから削除すると参照設定されるのでそのやり方がオススメ。
そしてメインの標準モジュール(名前は任意)を挿入し、以下のコードを張り付け。
'必要な参照設定 ' Microsoft VBScript Regular Expressions 5.5 ' ' Microsoft Forms 2.0 Object Library ' (こちらはフォームモジュール挿入して削除すれば参照設定される) Sub FromIndentStyleToCorrectVBAStyle() With New DataObject .GetFromClipboard Dim arr: arr = Split(.GetText, vbNewLine) End With Dim StatementStack As Stack: Set StatementStack = New Stack Dim IndentStack As Stack: Set IndentStack = New Stack StatementStack.Push "Dummy" IndentStack.Push -1 Dim i For i = LBound(arr) To UBound(arr) indents = GetIndentNumber(arr(i)) Do While indents <= IndentStack.Top indents = IndentStack.Top - 4 Debug.Print Space(IndentStack.Pop) & StatementStack.Pop Loop Debug.Print arr(i) es = GetEndStatement(arr(i)) If es <> "" Then StatementStack.Push es IndentStack.Push indents End If Next End Sub Function GetIndentNumber(codeLine) Dim n: n = 1 Do While Mid(codeLine, n, 1) = " " n = n + 1 Loop n = n - 1 GetIndentNumber = n End Function Function GetEndStatement(ByVal begin) Dim ret As String Dim re As RegExp: Set re = New RegExp begin = Trim(begin) re.Pattern = "^For .*" If re.Test(begin) Then ret = "Next": GoTo Fin End If re.Pattern = "^If .* Then$" If re.Test(begin) Then ret = "End If": GoTo Fin End If re.Pattern = "^Select Case " If re.Test(begin) Then ret = "End Select": GoTo Fin End If re.Pattern = "^Do" If re.Test(begin) Then ret = "Loop": GoTo Fin End If Fin: GetEndStatement = ret End Function
後はPythonみたいにインデントのみで書かれたコードをコピーして、FromIndentStyleToCorrectVBAStyleを実行すると、終了構文が補完されたコードがイミディエイトウィンドウに出力される。
一応作ってみたものの、私自身は毎日VBAに触ってるのであまりタイピングが面倒という感覚がなくなってきている。
ツール作成で一番時間を食うのは試行錯誤やデバッグなので、タイピングのロスは誤差の範囲。
だからまぁ、これを自分で使うことは無いだろうなぁ。
追記(言い訳)
今回はとりあえずアイデアを形にするためのやっつけコーディングなので、より良い方法があっても突っ込まないで欲しい。
たとえばスタックを2個使ってるけど、本当は関連項目だからユーザー定義型かクラスにしてデータをまとめて1つのスタックに積むか、スタック自体を改変して複数個を1領域にPushできる仕様にすべきだったと思う。
※Popが多値になるのでそれはそれで扱い注意だけど。
それと、冒頭で述べた既に終了構文があっても追記してしまうという重大な欠陥について、修正できなかった訳じゃないけど、このアイデアにそれほど思い入れが無いので放置することにした。もしこのアイデアを気に入った人がいたら直して使って欲しい。