VimとTUIツールをなめらかに切り替える
VimからTUIツールを呼び出す
私はVim以外のTUI(Terminal User Interface)ツールとしてファイラにRanger、Git操作にはTigを利用しています。
TUIはそれ自体の制約によりキーボード操作のみで完結することや、開発時の基本的なワークフローがターミナル内に閉じることによる環境のポータビリティにメリットがあります。
VimからはTUIツールの呼び出しは外部コマンド呼び出しの:!{cmd}
やVimの組込みターミナル:terminal {cmd}
でTUIツールを呼び出せます。
逆にTUIツールからエディタを呼び出すケースもあります。
たとえばファイラのRangerで選択したファイルを開く場合、環境変数$EDITOR
に登録されているエディタ(Vim)を使って開きます。
TigはGit操作でdiff表示しているときにe
を押したときのカーソルのある行でエディタを使ってファイルを開いてくれます。
Vimプロセスのネスト問題
VimからのTUI呼び出しやTUIからのVimの呼び出しを組み合わせると、Vimプロセスのネスト問題が発生します。
たとえばVimから外部コマンドでtigを:!tig
で起動し、さらにtig上で差分のあるファイルを選択してVimで開くとします。
このとき、プロセスツリー上では新しく開いたVimのプロセスがtigの子プロセスとして作られてしまいます。
bash───vim─┬...
└─tig───vim
この状態では、親のVimプロセスが持っていたbufferやyankが子プロセスのVimから扱うことができません。
また、:q
でどのVimプロセスが終了するのか、ネストされているのか気にしなければならないことも問題です。
一応使えはするけれども、Vimの利便性を大きく損ねている状態なので要改善です。
常に親プロセスのVimでファイルを開く
ネストしたプロセスを避けるには、外部コマンドを呼び出した場合でも、常に親のVimプロセスでファイルを開くことができれば解決します。
そこで、選択したファイルパスを一時ファイルに書き出し、それを親のVimが読み取って開けるようにしてみます。
手順は以下のようになります。
- TUIツールで選択されたファイルパスを一時ファイルに書き出し
- TUIツールを終了
- Vimで一時ファイルに記述されたファイルパスを開く
これらをシェルスクリプトとVim scriptで書いてみます。
独自処理を差し込める環境変数を探す
ファイルを選択して開くアクションがあるツールの場合、$EDITOR
のような環境変数で開くエディタを変更できる仕様になっていることが多いです。
TUIツールによって方法は変わりますので、外部エディタを起動するための環境変数をマニュアルから探します。
$ man tigrc | grep EDITOR -1
core.editor
The editor command. Can be overridden by setting GIT_EDITOR.
tigは$GIT_EDITOR
という環境変数に設定されたコマンドがエディタとして使われるように挙動を上書きできるようです。
GIT_EDITOR=myscript.sh tig
のようにtigを起動した場合、そのtigからファイルを開く際には myscript.shをエディタとして起動 しようとします。このときmyscript.shは引数でファイルパスや行番号などをtigから受け取ることができます。この動作を利用してファイルを書き出します。
TUI終了後のコールバック
callback_script.sh
というシェルスクリプトを作成します。
#!/bin/bash
echo edit "$1 $2" > /tmp/callback_file # $1が+行番号 $2がファイルパス
kill -1 $PPID # SIGHUPでtigを終了させる
TUIで選択したファイルパスと行番号を書き出し、親のtigプロセスを終了させるシェルスクリプトです。
ファイルパスと行番号を書き出す
tigはファイル差分をdiff表示しているときにe
を押したときのカーソルのある行でファイルを開いてくれます。
このとき、エディタコマンド($GIT_EDITOR
)に渡される引数の形式は、第一引数が +行番号 、第二引数が ファイルパス になります。
のちほどVim側で:edit +10 path/to/file
のように行番号指定でファイルを開くようにしたいので、一時ファイル/tmp/callback_file
に書き出しておきます。
TUIを終了させる
フォアグラウンドでtigが起動し続けてしまうとVimに操作が戻りません。何かしらの方法で終了させる必要があります。
そのため、tigにはSIGHUPシグナルを送ってtigを終了させます。(子から親を殺しているので行儀が悪いですね。)
$PPID
でシェルスクリプトの親となるtigプロセスを参照しています。
Vimで一時ファイルに記述されたファイルパスを開く
次は~/.vimrc
にOpenTig()
という関数を作成します。
function! OpenTig()
silent !GIT_EDITOR=path/to/callback_script.sh tig " tigの呼び出し
if !filereadable('/tmp/callback_file') " tigの操作を中断して戻ってきたとき
redraw! " 画面の再描画
return
endif
execute readfile('/tmp/callback_file')[0] " 一時ファイルの内容を実行
call delete('/tmp/callback_file') " 一時ファイルの削除
endfunction
先程作成したシェルスクリプトを環境変数にセットしてtigを呼び出します。
その後、一時ファイルに記述されたファイルパスを開きます。
tigでの操作終了後、/tmp/callback_file
へ書き出された内容をVimで読み取り、その内容を実行します。
(:edit +10 path/to/file
のような行番号とパス指定でファイルを開く内容が記述されているはずです。)
:call OpenTig()
でVimからtigを呼び出し、tigで選択した箇所を親のVimで開けるようになりました。
あとは~/.vimrc
に作成した関数をキーマップとして登録してしまいましょう。
nnoremap <silent> ,t :call OpenTig()<CR>
プロセスツリー上からtigが消え、Vimもネストしなくなりました。
bash───vim─...
常に親プロセスのVimでファイルを開くようにできたのでbufferやyankが途切れなくなり、冒頭のVimのプロセスのネスト問題は解決しました🙌
まとめ
少しのVim scriptとシェルスクリプトでVimプロセスのネストをなくすことができました。
これによりTUIツール間のシームレスな切り替えが実現し、Vimのワークフローがより快適になります。
私が作っているtig-explorer.vimや、ranger-explorer.vimは今回紹介したスクリプトからプラグインの開発をスタートしました。
土台はあまり今も変わっておらず、共通してどちらも以下のような機能を追加しています。
- TUIから
<C-v>
<C-s>
<C-t>
でsplit
,vsplit
,tabedit
して開く -
:terminal
のサポート - Neovimをサポート
TUIツールを連携させるときのテクニックは割と汎用的に使えます。
お気に入りのTUIツールをエディタと連携させていきましょう💪
Discussion