はじめに
neovim、version8.0以降のvimに実装されているterminal
機能を使いたかった。
なお、今回は8.1以降のvimを想定している(8.0→8.1で一部オプションや挙動が変わってたりする)
最新のvimを入れたかったらUbuntuなら
sudo apt remove vim
sudo add-apt-repository ppa:jonathonf/vim
sudo apt update
sudo apt install vim
とかでいけると思う。ほかの環境はちょっとわかんない。
画面下部に表示するように
terminalはデフォルトの設定だと画面上部に出てくる。なぜだ。
とりあえず下記の設定で画面下部に出るようになる。
set splitbelow
厳密にいうとこれは画面の水平分割時にいまのウィンドウの下に新しいウィンドウを生成するようになる設定だけど細かいことは気にしない。
デフォルトのターミナル、存在感大きくない?
ターミナルはデフォルトの設定だと画面を二分割して表示される。でかい。
ということで出てくるターミナルのサイズを指定。
set termwinsize=7x0
7が高さ指定、0が幅指定。1以上だと指定行(列)数サイズになる。0だと最大。間の文字はエックス。
ちなみにvimのドキュメントだとなぜか間の文字を*(アスタリスク)にするほうが強調されてたりするけどこれは最低サイズを指定するというあまりいらない子だった。
terminalがいっぱい出てきて邪魔
:terminal
は実行すればしただけ新たにウィンドウを生成する。そんなにいらない。
下記の関数ですでに存在すればterminalを生成しないようにしている。
function! TermOpen()if empty(term_list())
execute "terminal"endifendfunction
term_list()はvimの組み込み関数で、現在開いているterminalのバッファのリストが取得できる。
これが空の時だけターミナルを開く。これを適当なキーで呼び出せるようにする。
noremap <silent><space>t:call TermOpen()<CR>
とりあえずspace + tで実行できるように。この書き方だとわざわざキー入力を再現してしまうのでもっと賢い書き方があったら教えてほしい。
二個目以降は既存のターミナルへフォーカス!!
さっきの関数だと二個目以降は無視される。これはあまりよろしくないので下記のように拡張する。
function! TermOpen()if empty(term_list())
execute "terminal"elsecall win_gotoid(win_findbuf(term_list()[0])[0])endifendfunction
term_listでバッファ番号を取得、win_findbuf()でウィンドウのIDへ変換。win_gotoid()でそのIDのウィンドウへと飛んでいる。
bufid → winidの変換にはbufwinidもあるが、こちらは現在のタブしか探せないっぽかったのでこっちにした。
これで二個目以降を開こうとしたら一番初めに開いたターミナルにフォーカスするようになる。よって配列要素アクセスはハードコーディングしていても問題はない。え? 直接:terminal
を入力? 知らない子ですねぇ。
vimを閉じようとしたらターミナルだけ残って嫌な気持ちに
これを解決するためにかなり無理やりな実装をしている。あまり参考にしないでほしい。
(しかも完全には解決できていない)
アルゴリズムとしては、何かしらのウィンドウを閉じたときにterminalが開かれているか確認、そのウィンドウが開かれているタブのウィンドウの数を確認、残っているウィンドウの数が一つだけならterminalを終了、といった感じ。
まずはterminalが開かれているか確認
これは簡単。さっきもやってる。
if!empty(term_list())
これでいける。
terminalが開かれているタブで開かれているタブのウィンドウの数を確認
これがちょっとめんどくさい。まずterm_list()で帰ってくるのはバッファ番号だ。そしてvimにはバッファ番号からそのバッファが開かれているタブを取得する関数は用意されていない(たぶん。あったらごめん)。
しかし、タブ番号から開かれているウィンドウ数を取得する関数は用意されている。
tabpagewinnr(tabnr,'$')
'$' を指定しないとそのタブのカレントウィンドウ番号を返す。
つまり、バッファ番号からそのバッファが開かれているタブを見つけることさえできれば良い。
調べたら先人がいた。コピペOKとのことなのでありがたくコピペすることにする。
function! CreateBufnr2tabnrDict() abort
let bufnr2tabnr_dict ={}for tnr in range(1, tabpagenr('$'))for bnr in tabpagebuflist(tnr)let bufnr2tabnr_dict[bnr]= has_key(bufnr2tabnr_dict, bnr) ? add(bufnr2tabnr_dict[bnr], tnr):[tnr]endforendforfor val in values(bufnr2tabnr_dict)call uniq(sort(val))endforreturn bufnr2tabnr_dict
endfunctionfunction! Bufnr2tabnr(bnr) abort
return CreateBufnr2tabnrDict()[a:bnr]endfunction
解説はkoturn様にお任せする。わかりやすい説明をありがとうございます。
準備は整った!!
いまこそterminalをぶっ〇す時。
function! ExitTerm()if!empty(term_list())let term_tabnr = Bufnr2tabnr(term_list()[0])let num_win_in_tabnr = tabpagewinnr(term_tabnr[0],'$')if num_win_in_tabnr ==1call term_sendkeys(term_list()[0],"exit\<CR>")endifendifendfunction
term_list()でバッファ番号を取得、koturn様がタブ番号に変換、そのタブ内のウィンドウ数を取得、ひとつだけならぶっ〇す。
ちなみに〇す方法もうまい方法が見つからなかったのでterminal内でexitって打ってる。よろしくなさそう。
あとはこれを呼び出すだけ。
augroup term-exit
autocmd!
autocmd BufEnter * call ExitTerm()
augroup END
使うイベントはBufLeaveとかでも良さそう。
残った課題
上記までの方法だと、
- 現在のタブには作業用ウィンドウがひとつ、terminalウィンドウがひとつの合計二つ。別のタブが開かれている。
みたいなときには現在の作業用ウィンドウを閉じるとterminalも終了して次のタブに移動する。すばらしい。
しかし、
- 現在のタブには作業用ウィンドウがひとつ、terminalウィンドウがひとつの合計二つ。別のタブは開かれていない。
みたいなときだと、現在の作業用ウィンドウを閉じようとしたらterminalだけがサヨナラして現在のウィンドウは生存する。身代わりになるとは...やるなterminal...
仕方がないのでそういう時は魔法のコマンド。
qa!
万事解決。(作業用のウィンドウの保存し忘れとかはauto-saveが解決する)。
まぁタブが残ってるときの挙動だけでもうまくいったので満足。眠くなってきたのでおわりにした。もうちょっとましな方法があったら共有する。