Vim の組込み Ruby に編集中のコードを実行させる

Vim上でRubyを動かしたい。 - ボクノス

  • 素敵なところ
    • キーマップがかっこいい。ここには載せてないけど元記事の matchit.vim 対応と合わせると、素敵度がさらに上がる。
    • 実行結果を覚えていてくれる。
  • 残念なところ
    • 実行結果を覚えていてくれる。というか、忘れてくれない。
    • 実行結果が、シェル上で実行したものと、必ずしも一致しない。例えば require "pp" としても、うちの環境では pp が使えなかった。
    • 実行結果がコマンドライン上に表示されるので、結果を眺めながらコードをいじる事が出来ない。

上記の特徴から、この方法は完成したコードの実行結果を見るためというより、融通の利く irb という感じで使う事になる。改造を施したのは 1番目の不満点。F10キーを押すと、それまでの実行結果(定義したクラス、ローカル変数)等を破棄する。グローバル変数は破棄出来ない。

"選択範囲のスクリプトを実行。
"http://d.hatena.ne.jp/tanakaBox/20070827/1188149288
nmap <buffer><silent> <C-CR> :call <SID>Eval(function("<SID>YankNormal"))<CR>
vmap <buffer><silent> <C-CR> :call <SID>EvalVisual()<CR>
nmap <buffer><silent> <f10>  :call <SID>Create_ruby_binding()<CR>

" yank で取り込んだコードを実行するコンテクストを作成する。
"
" この Create_ruby_binding() を実行すると toplevel の self (= main) を複製す
" る。複製されたインスタンスは、自分自身の文脈を持った binding をインスタンス
" 変数として持つ。
" eval にこの bindig を第2引数として渡すと、eval に渡された文字列はこの複製さ
" れたインスタンスの文脈で実行される。
" もう1度 Create_ruby_binding() を実行すると、新たにインスタンスが複製され、代
" 入されなおすため、それまでの文脈情報が破棄される。
function! s:Create_ruby_binding()
ruby << EOR
toplevel = self.clone
class << toplevel
	def create_binding
		@binding = binding
	end
	def context_inf
		@binding
	end
end
toplevel.create_binding
ruby_box = toplevel
EOR
endfunction

call <SID>Create_ruby_binding()

function! s:Eval(Yank)
	let s:saved_reg = @"
	call a:Yank()
	execute 'ruby begin; p eval(%q(' . @" . '), ruby_box.context_inf); rescue ScriptError, StandardError => e; puts e.message;end'
endfunc

function! s:EvalVisual() range
	call <SID>Eval(function("<SID>YankVisual"))
endfunc

function! s:YankVisual()
	silent normal `<v`>y
endfunc

function! s:YankNormal()
	silent normal yy
endfunc

使い方

$home\vimfiles\ftplugin\ruby.vim(Windows の場合)に上記コードを追加。
詳細な操作方法は本家を参照。付け加えたのは、F10キーで文脈リセットのみ。

調べた事/やり残した事

現在までの道のり

こちらの方法の利点、「実行結果を覚えていてくれる」は確かに便利なのだが、未知のメソッドの挙動を観察しているときには、それまでの実行結果をリセットしたくなる事がある。ところが、Ruby には一旦定義したローカル変数を未定義にする事が出来ないらしい。
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/2522
*1
定義されているものに、片っ端から nil を代入しようかとも思ったが、未定義の変数と nil を持った変数はやっぱり違う。
元のスクリプトを眺めると eval の第2引数に __vim_bind という変数を渡している。eval なんか普段使わないんで binding というものも知らなかったのだが、調べてみるとこいつをいじくれば色々な事がなかった事に出来そうだ。最初は toplevel で作った binding を dup したり clone したりして代入しなおしてみたのだが、思ったような結果にならない。
次にこの文脈を保存しておくためだけのクラスを作って(文脈をクラス内に封入する)、文脈を捨てるには、そのクラスとクラスから生成されたインスタンスを破棄すればいいんじゃないかと考え、以下のように試してみた。

# このコードが Create_ruby_binding()の度に呼ばれる。
ContextInformation = Class.new
class ContextInformation
	def initialize
		@binding = binding
	end
	def context_inf
		@binding
	end
end
ruby_box = ContextInformation.new

すると、文脈を捨て変数を未定義に戻す事が出来た。のだが、この方法だと、編集中のコードの見かけ上の toplevel はこのクラスのインスタンスという事になる。大抵の場合困らないのだが、このインスタンスには include メソッドが定義されていないので、toplevel では include が使えない事になる。
http://www.ruby-lang.org/ja/man/html/Ruby_CDD1B8ECBDB8.html#main
それほど困らないとはいえなんだかしゃくなので、include をどうにかして委譲できないかと色々試して挫折。上記用語集の main の項目を眺めていたら

単なるObjectクラスのインスタンスである

のなら、そのインスタンスを複製して、その複製に binding を生成する特異メソッドをつけてやればいいんじゃね? と思いついて、書かれたのが上のもの。

積み残し

あわわ。4/12に id:tanakaBox コメントをもらってたようです。お知らせメールに今頃気付きました。削除などした覚えはないのですが…。
内容は以下の通り。

tanakaBox
『こんにちは、「実行結果をプレビュウウィンドウに」ってやつですが、redirを使うと、変数に保存出来ます。

http://d.hatena.ne.jp/tanakaBox/20080331/1206927370

のReadirect関数使えば何とかなるかなぁと思います。Scheme用ですけど、参考にしてください。』

わざわざ、どうもありがとうございます。
やっぱこういう方法がありますよね。というか、:redir 以前使った覚えがあるなぁ…。
と、いうわけで、以下に記述された方法を使うべき場面は、あまりないと思われます。

あと、実行結果をプレビュウウィンドウに表示させたかったのだけど、挫折。
例えば :ls の結果をバッファに流し込むのってどうやるんだろう。可能なんだろうか? 多分方法はあるのだろうけど、例によって:h の迷宮で屍となる。
Vim 側のいじり方が分からないので、Ruby 側をいじって解決しようと試みた。
Ruby の p, puts, print などのメソッドの出力先は $stdout であり、この変数に write という名前のメソッドを持ったオブジェクトを代入してやれば、出力先はそのオブジェクトになるはずである。
http://www.ruby-lang.org/ja/man/html/_C1C8A4DFB9FEA4DFCAD1BFF4.html#a.24defout
というわけで、Vim 側で定義されている Vim のバッファを表すクラス VIM::Buffer に以下のように write メソッドを加えた。

ruby << EOR
class VIM::Buffer
	def write( str )
		self.append(self.length, str)
	end
end
EOR

そして、カレントバッファのインスタンスを $stdout に代入してみた。すると、puts, print メソッドの出力結果は、余計な改行が1つずつ付いているものの、カレントバッファに出力する事が出来た。ただ p メソッドの出力だけは、カレントバッファにではなくコマンドラインに出力されてしまう。なぜ p だけ…? といったところで、今回はあきらめる。

*1:日付が10年前のものだからもしかしたら状況は変わっているのかもしれないが、もしあるとしてもその方法を見つけられなかった。