🐖

VimとTUIツールをなめらかに切り替える

2021/12/14に公開

VimからTUIツールを呼び出す

私はVim以外のTUI(Terminal User Interface)ツールとしてファイラにRanger、Git操作にはTigを利用しています。

https://github.com/ranger/ranger

https://github.com/jonas/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が読み取って開けるようにしてみます。

手順は以下のようになります。

  1. TUIツールで選択されたファイルパスを一時ファイルに書き出し
  2. TUIツールを終了
  3. 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で一時ファイルに記述されたファイルパスを開く

次は~/.vimrcOpenTig()という関数を作成します。

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で開けるようになりました。

Image from Gyazo

あとは~/.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は今回紹介したスクリプトからプラグインの開発をスタートしました。

https://github.com/iberianpig/tig-explorer.vim
https://github.com/iberianpig/ranger-explorer.vim

土台はあまり今も変わっておらず、共通してどちらも以下のような機能を追加しています。

  • TUIから<C-v> <C-s> <C-t>split, vsplit, tabeditして開く
  • :terminalのサポート
  • Neovimをサポート

TUIツールを連携させるときのテクニックは割と汎用的に使えます。
お気に入りのTUIツールをエディタと連携させていきましょう💪

Discussion