反省はしても後悔はしない

Vim とか備忘録とか。それと関数型言語勉強中

Vim のバッファとウィンドウを理解する

この記事は Vim Advent Calendar の 302 日目の記事です。昨日は id:rattcv さんの Vimから電話がかけられるなんて・・・素敵! でした。すごいですね。

はじめに

Vim には強力なウィンドウ分割機能があります。一方で、プラグインなどのドキュメントなどではバッファという表現が使われることが多いです。この2つは何者なのでしょう?両者の違いは? 私ははじめの頃、ウィンドウ、バッファという2つを混同していました。他にも混同している人が多いのではないかと思ったので今回はこの2つについて解説したいと思います。

バッファとは?

基本的には1つのバッファ=1つのファイルです。ですが、プラグインなどでは必ずしもファイルとは一致しません。例えば、Unite のウィンドウを開いた場合にもバッファが作られます。メモリ上に読み込まれた(ことがある)内容に対応するものがバッファ、というのが正確な表現でしょう。

例えば、下記のような Ex コマンドを連続して打ったとしましょう

edit hoge.html
edit fuga.css
edit piyo.js

このとき、ウィンドウはひとつだけですが、バッファは3つある状態になります。 バッファの状態は :ls コマンドで確認することができます。

ウィンドウとは?

ウィンドウは今まさに目に見えている領域で、バッファの内容を表示するものです。

例えば、下記のような Ex コマンドを打ったとしましょう。

edit hoge.txt
split

このコマンドを打つと、hoge.txt の内容が水平分割されて表示されます。そして、片方を変更するともう片方にも自動的反映されます。 この場合、1つのバッファ(hoge.txt)に対して2つのウィンドウが対応します。一般的に、1つのバッファに対して0以上のウィンドウが対応する感じです。(つまり、1対多の関係)

さて、:q はウィンドウを閉じるコマンドです。なので分割してウィンドウが2つある場合は片方のみ閉じられてもう一方は残ります。対して、:bdelete, :bwipeout はバッファを削除するためのコマンドです。1つのバッファを分割している状態で :bdelete すると、バッファ自体が削除されます。そして、それに紐づく2つのウィンドウがいっぺんに閉じられることになります。

(ついで)タブページとは

ウィンドウを束ねるものがタブページです。ちなみにですが、単にタブというと <Tab> 文字のことになるので区別したい時にはタブページと言いましょう。 タブページはウィンドウを束ねているだけなので、タブページごとにバッファを管理したいとかそういうことは現状ではできません。 (https://github.com/vim-jp/issues/issues/342)

バッファローカル、ウィンドウローカル

前述したとおり、バッファは(ほぼ)ファイルに対応します。なので、ある特定のファイルだけに設定したいものはバッファローカルな変数やオプションを使います。例えば、hoge.html というファイルを開いたときは HTML として認識して欲しいし、fuga.css というファイルを開いた時には CSS として認識して欲しいですね。こういう場合には filetype というバッファローカルなオプションが使われていて、各ファイルに対して別々の設定をさせることができるようになっています。他にも、b:did_ftplugin という変数で filetype plugin が読み込まれたかどうかを記録するといったことや b:quickrun_config であるバッファだけ QuickRun の設定をするなどのように使われています。

また、見た目に関する設定にはウィンドウローカルな変数やオプションを使います。例えば、行番号を表示する number というオプションはウィンドウについてローカルな値を持つことができます。つまり、1つのバッファを分割して開いているときに、片方だけ行番号を表示しないといったことが可能です。他にも、indent-guides というプラグインではインデント量を可視化する(つまり見た目)ための設定をウィンドウローカルな変数で持っています。

ちなみに、オプションがバッファローカルまたはウィンドウローカルな値を持つかどうかはすべてヘルプに書いてあります。:help options もちろん、ignorecase などのようにグローバルな値しか持たないものもあります。

バッファローカル、ウィンドウローカルな変数を Vim script から操る

これらのローカル変数を Vim script から操ることができるといろいろ便利です。自分が今いる位置のウィンドウ、バッファのローカル変数は b: や w: を使うことで参照できます。では別のウィンドウやバッファの変数にアクセスするにはどうすればいいでしょうか。そんな時のために Vim の組み込みの関数が使えます。

関数名 説明
winnr('.') 現在のウィンドウ番号を返す
winnr('$') ウィンドウの個数を返す
bufnr('%') 現在のバッファ番号を返す
bufnr({expr}) {expr}という名前のバッファの番号を返す。{expr} はファイル名とか。
getwinvar({winnr}, {varname}[, {def}]) {winnr}という番号のウィンドウの {varname} というウィンドウローカル変数の値を返す(w: を付けない名前)。存在しない場合は {def} (指定した場合) または空文字を返す
getbufvar({expr}, {varname}[, {def}]) {expr} という名前または番号のバッファの {varname} というバッファローカル変数の値を返す。(b: を付けない変数名) {expr} は文字列か数値。存在しない場合は {def} (指定した場合) または空文字を返す
winbufnr({nr}) {nr} という番号のウィンドウで開かれているバッファの番号を返す。
bufwinnr({expr}) {expr} という名前または番号のバッファの最初のウィンドウの番号を取得する。

例えば、help を開いているウィンドウをフォーカスを動かさずに閉じるとかできるようになります。

function! CloseHelpWin()
  for w in range(1, winnr('$'))
    let bt = getwinvar(w, '&buftype')
    if bt ==# 'help'
      execute w . 'wincmd w'
      q
      break
    endif
  endfor
endfunction

help 閉じるが果たして便利かは置いといて、これを応用すると例えば Quickrun の出力ウィンドウとか diff してるときのもう片方とかも移動せずに閉じるということができますね。便利。

もっといいのないの?

Vim のバッファ、ウィンドウ系の関数はここに紹介した以外にもいくつかあるのですが、微妙に使い勝手が悪かったりします。id:osyo-manga さんが書いた vim-gift を使うとバッファ、ウィンドウの操作をもっと便利にできるでしょう。

明日は supermomonga さんです。