Kotaro7750's diary

低レイヤを中心とした技術ブログ、たまに日記

Neovim内でのターミナル利用を快適にするVimscript

以下のリポジトリに設定ファイルが全て載ってるので参考にどうぞ。 github.com

Terminalモードについて

公式にもあるように、Neovimではターミナルエミュレーターモードが備わっていて、常にvimの中にいながら作業ができます。 本家のvimにおいても

echo has('term')

の結果が1ならば、ターミナルモードが有効化されています。 まさにこれからのvimmerにとっては必須のモードであると言えるでしょう。

デフォルトのターミナルモード

:term

コマンドでターミナルバッファが開きます。

ここで少し注意があって、ターミナルモードにおいてもvimにおけるノーマルモードやインサートモードといったモードの概念があり、初見だと混乱するかもしれません。

タイプするキー 動作
i インサートモードに移行
Ctrl+\ Ctrl+n ノーマルモードに移行

f:id:Kotaro7750:20191108113646g:plain

デフォルトでの挙動

また、:termコマンドを打つたびに違うシェルが開いてしまい、元いたシェルに戻りたいという場合にはいちいちバッファから探さなくてはいけないため、そのままだと正直あまり使い勝手が良くないです。

快適にしたターミナルモード

デフォルトでの不満点がいくつかあげられたため、それらを解消するVimscriptを書きます。 今回紹介する機能は以下のようになります。

  1. ターミナルに移るキーコンフィグを変える(Space + t)
  2. ノーマルモードに戻るキーコンフィグを変える(Space + t)
  3. 開いているシェルは常時1つに固定し、常に同じターミナルに戻れるようにする

f:id:Kotaro7750:20191108114339g:plain

1,2に関しては簡単なので今回は主に3について説明していきたいと思います。

"---terminal---
set shell=/bin/zsh
tnoremap <silent> <ESC><ESC> <C-\><C-n>
nnoremap <silent> <Leader>t :call ToggleTerminalMRU()<CR>

let g:mru_buffer = 1
let g:mru_buffer_prev = 1
autocmd bufleave * let g:mru_buffer_prev = bufnr()
autocmd bufenter *  call SaveMRUBuffer()

"exec when enter
function! SaveMRUBuffer() abort
  if IsNormal(g:mru_buffer_prev) && IsMemo(g:mru_buffer_prev) == 0
    let g:mru_buffer = g:mru_buffer_prev
  endif
endfunction

function! IsNormal(buf_num) abort
  if (buflisted(a:buf_num) == 1) && (IsTerminal(a:buf_num) == 0)
    return 1
  endif
  return 0
endfunction

function! IsTerminal(buf_num) abort
  let l:term_buf = bufnr("terminal.buffer")
  if a:buf_num == term_buf
    return 1
  endif
  return 0
endfunction

function! ToggleTerminalMRU() abort
  let l:cur_buf = bufnr()
  let l:term_buf = bufnr("terminal.buffer")
  if cur_buf == term_buf
    if bufexists(g:mru_buffer) == 1
      execute('buffer '.g:mru_buffer)
    else
      :echo "does'nt exist restorable editor"
    endif
  else
    if term_buf == -1
      execute("terminal")
      execute("f terminal.buffer")
    else
      execute('buffer '.l:term_buf)
    endif
  endif
endfunction

結構長いですが、大まかな仕組みとしては、 * Bufferを離れるときにバッファの番号を一時保存 * Bufferに入るときに、最後に使ったのが通常の(後述)バッファだったなら、一時保存していたバッファ番号を正式に保存。そうでなかったなら保存しない。 * ToggleTerminalMRUが呼ばれたら、 1. 今いるバッファがターミナルなら、最後にいたバッファに移る 1. 今いるバッファがターミナル以外なら、ターミナルバッファに移る 1. ターミナルに移る際は、ターミナルバッファがすでにあるならばそこに移り、そうでないなら新しく作る

というようになっています。

以下にそれぞれの関数の仕組みについて解説していきます。

SaveMRUBuffer関数

前提として、g:mru_buffer変数は「ToggleTerminalMRUで戻るバッファ番号」、g:mru_buffer_prev変数は「直前に開いていたバッファ番号」となっています。 そして、g:mru_buffer_prevはBufferから離れるときに、そのバッファの番号で更新されます。

SaveMRUBuffer関数は、以前開いていたバッファが通常のバッファであるならば、g:mru_buffer変数を更新する関数です。 つまり、ToggleTerminalMRUで戻る先のバッファを更新する関数です。 この関数はbufferに入ったときに呼ばれるようになっておりIsNormal関数・IsMemo関数で以前開いていたバッファが「通常」のバッファであるかどうかを判定し、 そうであるなら、g:mru_buffer変数にg:mru_buffer_prev変数を保存します。

通常のバッファとは

IsNormal関数は、「バッファのリストにあり、ターミナルではない」場合に1を返します。 つまり、「通常の」バッファとは、ターミナルのバッファ以外のすべてのバッファです。 また、IsMemo関数というのは、これも私が作った関数で、ターミナルと同様にメモ用のバッファを出し入れする機構を作成した際に実装したものです。メモのバッファである場合に1を返します。

これらを総合すると、通常のバッファとは「バッファのリストにあり、ターミナル・メモでないバッファ」となります。

わざわざSaveMRUBuffer関数を用意したのはターミナルバッファ・メモバッファを直前までいたバッファとして認識させないためです。 もしそのように認識させてしまうと、メモを経由してターミナルに移った場合に、メモバッファの方に移ってしまい、私の意図する挙動ではなくなってしまいます。

ToggleTerminalMRU関数

ToggleTerminalMRU関数はターミナルと直前までいた通常のバッファの間を行き来するための関数です。今回はSpace+tで発火させています。

現在のバッファがターミナルの場合

SaveMRUBuffer関数で、直前の通常バッファがg:mru_buffer変数に保存されているので、それがあるならばvimのビルトイン関数であるbuffer関数を使い、移ります。 もし、バッファが消されてしまっていた場合には映らないという挙動にもなっています。

現在のバッファがターミナル出ない場合

この部分がデフォルトのターミナル機能での、新しいシェルが常に開かれてしまうという問題に対応する部分となっています。

これを解消するために、新しくターミナルを開いたときに、ビルトイン関数のf関数を使ってバッファの名前を固定してしまい、 作ったあとはその名前で検索することでターミナルがすでにあるかを調べることができます。

まとめ

この改造を施したおかげで日々のvimライフが快適になりました。 私のリポジトリには他にもいろいろな設定がおいてあるのでぜひ活用してみてください。