はじめに
RubyKaigiとの連動企画ということで、先日行ったRubyKaigi 2023でのLTのセルフ解説をしたいと思います。
題材
rspec-current.vim
という、カーソルの位置に応じたsubject
やcontext
の内容を返すRSpec用VimプラグインをRubyで書きました。LTではこのプラグインの解説をメインに行いました。
実装の解説
プラグインの名前こそ.vim
ですが、中身はほぼ全てがRubyで書かれています。特にRubyVM::AbstractSyntaxTree
クラスを使うところが肝で、ASTを取得することでそこから構文要素(例えばsubject { described_class.call }
における"described_class.call"に当たる部分)を取り出すことができます。この際、Ruby3.2で追加されたkeep_tokens: true
オプションを利用する必要がありました。
トーク中で流したnode.children.last.children.last.tokens.map { _1[2] }.join
の部分ですが、最初のnode
がsubjectを包むITER
ノード、そのchildren.last
でSCOPE
ノード、そのさらにchildren.last
でCALL
ノード、そこにtokens
メソッドを呼ぶことで各要素が配列の配列として返ってきます。各要素はその3番目の要素がトークンの文字列であるような配列であるので、_1[2]
でそれを取得してjoin
すると元のソースコードに近い文字列を得ることができます。このへんの詳細は金子さんの記事を参照するとわかりやすいです。
簡単なの?
発表の中でこのプラグインのようなものを作ることは"Easy"だと言いましたが、実際はどうなのでしょうか。たしかに、実装としてはASTをいじる以外に難しいことはやってはいません。
ただし、実装している最中はASTを手探りするしかありませんでした。各ノードの構造についてはドキュメントが一切なく(意図的なものだと思います)、そのためまずはRubyだけのVimとは無関係なファイルを用意してそこで色々実験して、得られた知見をVimプラグインに適用する形で実装を進めました。
rubyeval
を用いる方法も、一度気づいてしまえばなんてことはないのですが、最初はRubyからVimに値を返す方法がわかっていませんでした。このへんはVimプラグインを書いたことがないということが大きいですね。