動機
研究で使っているソフトウェアのREPLが少し使いづらい。というのも、制御文字がそのまま表示されてしまうため、十字キーのカーソル移動やCmd + Aの行先頭移動、Cmd + Kの行削除など効かないからだ。 rlwrap を使えばこの問題を解決できるのだが、別の解決案としてNeovimのREPL支援プラグインを作ってみようと思い立った。 Neovim のターミナル機能を使ってREPLを起動し、別バッファー上で入力した文字列をREPLに送るようなプラグインを作りたい。
GitHubで検索してみると同様の機能を実現するプラグインはいくつもあるようだが(例えば、vim-slime)、プラグインを作る勉強として、自分で作ってみる。
提供する機能
使用感をvlimeと似たものにしたい。キーマップは次のようにする。
<LocalLeader>ss
カーソル下の行をREPLに送る。<LocalLeader>s
選択範囲内行の文字列をREPLに送る。<LocalLeader>i
1行入力用のバッファを表示し、そこで書いた文字列をREPLに送る。<LocalLeader>cd
Ctrl + D
をREPLに送る。<LocalLeader>cc
Ctrl + C
をREPLに送る。
その他、以下のコマンドを定義する。
ReplOpen [cmd]
: REPLを起動する。例えばReplOpen python
ならpython
のREPLが起動する。[cmd]
には任意のコマンドが入れられるため、REPL支援というよりターミナル支援プラグインという感じがするが、気にしないことにする。ReplSend [string]
: 文字列[string]
をREPLに送る。
補足 vlimeとキーマップが被るため、vlimeを入れている人はどうするんだという事になる。
その場合、キーマップが被らないようにしたり、ftplugin
下にスクリプトを書くなど色々と方法が考えられる。
ここでは一番無難そうな、「ReplOpen
が呼び出された時にキーマップを登録する」という方法を採用する。
注意
普段使っているのがVimではなくNeovimなので、Neovimを使ってプラグインを書く。Vimには無い関数/機能を使うので注意。
あとVim scriptをほとんど書いたこと無いため、今回載せるコードには色々改善点があるだろう。
準備
適切なディレクトリにプラグインのディレクトリを作成。自分の環境では、packpathの1つに~/.config/nvim
があったので、
~/.config/nvim/pack/plugins/start/
に置く。プラグインのディレクトリは愚直にrepl.nvim
とする
そこにautoload
、plugin
、ftplugin
ディレクトリを作成する。
|
|
REPLの起動
autoload/repl.vim
に色々関数を定義する。
まずはREPLの起動から。ウインドウを分割し、ターミナルを起動し、コマンドcmd
を実行する関数は素朴には以下のように書ける。
|
|
「a:変数名
とかs:変数名
って何?」と初めは思ったが、これの答えは:h internal-variables
にある。
a:
は関数の引数を表し、s:
はこのスクリプト内の変数を表す。
function
に!
をつけた場合、同名の関数があった場合に上書きする。abort
がついた場合、エラーが発生した際に関数をすぐに抜ける。これは:h user-function
に載っている。
(注意) termopen
関数はVimには無いはずなので、この時点でNeovimでないと動かない。Vimだとterminal
コマンドを
使えばターミナルが起動できるはずだが、詳しくは未調査。
後々ターミナルに文字列を送るなどをしたいため、バッファーやジョブの情報を保存しておきたい。 さらにカーソルがターミナルのウインドウに持っていかれないようにもしたい。
以上を踏まえると、以下のように書けば良いだろう。
termopen
関数はジョブのIDを返り値とするので、それをs:repl_id
に保存。
bufname
を無引数で呼び出すと現在のバッファー名を返すので、それをs:repl_buffer
に保存。
元のウインドウに戻るために、一旦win_getid
に元のウインドウのIDを保存しておいて、最後にwin_gotoid
で移動する。
|
|
ターミナルが終了した場合の処理も付け加えておこう。REPLが動作中かどうかをフラグs:is_repl_active
で管理し、終了した場合はフラグを0
にする。
実はtermopen
関数のイベントハンドラとしてon_exit
が指定できる(他のハンドラについては:h termopen
を参照)。
Vim scriptでは関数と変数の名前空間が別らしく、関数の参照を取得するにはfunction
関数を使う。
というわけで以下が最終形。
|
|
実際に:source autoload/repl.vim
で読み込んで、repl#open
関数が正しく動くかテストしてみると良い。
以下は:call repl#open('python')
でPythonのREPLを起動した例。
デバッグ用に、保存したジョブIDとバッファ名を返す関数を作っておく。
|
|
文字列をREPLに送る
基本はこれだけである。ターミナルに対してchansend
で文字列を送れば良い。
|
|
としたいところなのだが、REPLが1度も起動していないときにこのコマンドを打つと以下のエラーが出る。
|
|
REPLが起動していない時はs:is_repl_active
が未定義なのである。そのため、定義済みかどうかもチェックする必要がある。
実はs:is_repl_active
は、s:
という辞書のキー'is_repl_active'
としてアクセスできる(:h internal-variables
参照)。
get
関数を使えば、辞書のキーが存在しなければデフォルト値を返すようにできるので、次のように書き変えれば今回の問題は解決できる。
|
|
ただしこれだけだと、末尾に改行が加えられないので不十分。なので改行を加えて送信する関数repl#sendln
も作成する。
ここで注意したいのはchansend
の引数で、:h chansend
によると、第2引数に指定するデータは文字列、文字列のリスト、Blob
の3つの可能性がある。今回送りたいのは文字列なのでBlob
は無視する。それぞれ次のように送信されるらしい。
- 文字列はそのまま送信。
- 文字列のリストの場合、それぞれの要素が
\n
で連結されて送信。
これを踏まえて、2つの場合で処理を分ける。変数の型はtype
関数で取得できる。型名はv:t_型名
で扱われている。
詳しくは:h type
を参照。
文字列の連結は.
で行える。リストの追加はadd
関数で行える。
|
|
現在のカーソル下の行をREPLに送る関数は次のように書ける。getline('.')
でカーソル下の行を取得できる。
|
|
選択範囲をREPLに送る関数は次のように書ける。関数定義にrange
を指定すると、a:firstline
、a:lastline
で選択行が取得できる。
|
|
ターミナルを最下に自動スクロールさせる
ターミナルに文字列を送っていると、画面下へとはみ出てしまい見えなくなる。そのため、ターミナル画面を自動で下にスクロールさせる仕組みが必要である。
これを実現するためには、素朴には以下の手順を踏めば良い。
- ターミナルのウインドウに移動。
$
コマンドを実行してカーソルを最下に移動。- 元のウインドウに戻る。
実はターミナルに移動する手間を省くことができて、それにはwin_execute
関数を使えば良い。これは指定されたウインドウに対してコマンドを実行する関数である。
s:repl_buffer
が存在するか、ウインドウが存在するかなどの確認処理を加えると、最終的な関数は以下のようになる。
|
|
画面がスクロールする処理は、ターミナルに文字列が出力された際に行われるようにしたい。
そのためのイベントハンドラをtermopen
関数の引数に指定。
|
|
入力用バッファーの作成
画面下に1行入力用のバッファーを表示させたい。以下のような、2行の高さを持つバッファーを作る。
|
|
画面の高さを指定した新しいバッファーを作るためには、[高さ]split [バッファ名]
とコマンドを実行すれば良い。
|
|
出てきたウインドウは、デフォルトだとおそらく画面上に表示される。これを下に持っていくためにbotright
と組み合わせる。
|
|
以上で適切な高さのバッファーが作れた。続いて文字列Input
と空行を挿入。カーソルを2行目に移動させる。
|
|
さて入力した文字列をREPL送信する関数を作っておく。バッファの2行目を送信し、bwipeout
でバッファを閉じる。
|
|
開いたバッファーに対してキーマップを登録する。qキーが押されたときはバッファを閉じる。
Enterキーが押された時はsend_input()
を実行。スクリプトローカルな関数(s:
がついた関数)をコマンドに登録する際には、s:
ではなく<SID>
を指定する。map
系のコマンドはたくさんあるので詳しくは:h map-commands
参照。
<C-u>
は数字や範囲指定などの余分な文字列を削除するために用いている。これは:h c_CTRL-U
に載っている。
|
|
以上をまとめると、入力バッファを開く関数、入力文字列を送信する関数は次のようになる。
すでにウインドウが存在した場合は移動するように条件分岐を行っている。
文字列を展開してコマンドを実行するためにexecute
コマンドを使っている。
|
|
コマンド・キーマップの登録
まず、autoload/repl.vim
側で、キーマップを登録する関数を作っておく。
|
|
plugin/repl.vim
を編集する。まずプラグインが二重ロードされるのを防ぐための文を書く
(このイディオムは:h write-plugin
のNOT LOADING
の項に載っている)。
|
|
目的のコマンドを登録する。command
コマンドで複数のコマンドを実行したい場合、-bar
オプションをつけて|
で区切ってコマンドを記述する点に注意。ReplOpen
コマンドでは、REPLを開く関数とキーマップを登録する関数の両方を呼び出している。
|
|
<LocalLeader>
について。自分で対応するキーを、init.nvim
に設定しておく。例えば<LocalLeader>
にスペースキーを割り当てたいなら、以下のようにする。
|
|
完成したもの
完成品を、一応GitHubのRepositryに上げておいた。