buffer, window and tabpage

tabpage と window に関する考察 (?) の部分を修正。そもそもの前提が間違っているよと id:thinca ( いつもありがとう ) に教えてもらったのでがりがりと推敲。

最近 autocmd まわりをいじってて buffer だとか window だとかをきちんと理解しないといけないなと思ったので ( やはり ) help を読みながら自分なりにまとめ。てか tabpage つかってると 1 file / 1 tabpage で済むので普段の operation では buffer とか window とか意識する必要ないんだよな ( help も空の tabpage で開いて <C-w><C-w><C-w>c するタイプ。ってこれ :tab help tabpage とできるのに今回気付いた ) 。もうちょっというとそもそも file の編集をするだけなのに buffer とか window とかいう概念持ち込んで複雑にすんなよと思ってました最初は。あと help 引いて <C-w> で始まる normal command の数の多さにひいたりとか。

とりあえずこの entry 書くのに、というか関連する help を読むのに 1 週間かかったという事実が。まぁ知らないこと見つけるたびにいろいろ試したり code 書いたりと寄り道の方が多いんだけどそれにしても使うのに手間のかかる editor だねこいつは。 default 設定で快適に使えるという思想に逆行しまくってやがる。その分可能なことも多いんだけど、ってか感覚まで tuning できる editor なんてそうそうないんだけどさ。

  • buffer
    • :help buffers
    • memory 上に load された file のことを buffer というらしい。この概念は他の editor でも普通に使われているね、というかたいていの editor だとこの概念のみで済むのでそもそも名前がついてないことが多いんだけど vim では明確にこれを buffer と呼ぶということだね。
    • buffer には active, hidden, inactive の 3 つの状態があるということだけど active はわかりやすいというか編集といった場合に通常考える形態なのでいいとして hidden と inactive についてはちゃんと感覚として持たないとダメな気がする。
    • まず hidden だけど command や実態を見てみる限りでは制御下にはあるんだけど screen に収まりきらない buffer を一時的に見えなくするために使う、というような用途かな。いくつかの file を扱っていて screen がちょっと手狭になったときや大量に編集すべき file がある場合に順番に処理するためにその時点で注力すべき file 以外は隠しておく、というのが考えられる。でもこれ使うのに注意が必要というか、人間の構造上目に見えないのは存在しないものと扱ってしまうことが多いので積極的に使いたくはない機能だよな。 'hidden' の help ( :help 'hidden' ) にも hidden buffer があることは忘れやすいから ":q!" や ":qa!" を使うときにはよく考えろと書いてあるし。とりあえず 'hidden' が set されていない限りは明示的に ":hide" を使わなければ hidden buffer にはならないみたい ?
    • inactive はまたややこしいんだけど一回何らかの file が読み込まれたあとに閉じられた ( ":close", ":quit" ) buffer ということらしい。表示されないし中に何も持ってない buffer と書いてあるので「閉じられた buffer 」というのは誤解を招く表現かもしれないけど閉じられただけであって buffer として存在はしている、ということだね。で、これがあると何がうれしいかというと ( 多分 ) ":buffers" で以前何を触っていたかがわかる、というのとそれに付随して ":buffer" で簡単に load し直せるというのがひとつ。あとは "complete" に "u" を指定しているときに keyword completion の候補を inactive buffer から探すというのもあるみたい。まぁ前者はともかく後者は人によっては補完効率が落ちると思うので ( 'complete' の default は ".,w,b,u,t,i" ) 適宜 :set complete-=u すべしということかな。
    • ":buffers" は ":args" とは違って filelist の指定ができないんだね。これはあらかじめ名前のわかっている複数の file を横断的に編集するなら明示的に :args 系列の command をつかえ ( すでに buffer list に何かある場合でも使える ) ということだろうけど vim の仕組み上 ":args" で指定した file 群はすべて buffer list に登録される ( vim は file をすべて buffer という単位で扱う ) ので :args 一発ですべての file を指定しきれない場合なんかは buffer list を空にしたあと複数回 ":args" を打って ":bnext" や ":bufdo" を使えばよい、のかな ?
    • ":bunload", ":bdelete", ":bwipeout" の順に影響範囲が広がっていく。 ":bunload" ( というか hidden buffer を考慮しなければ ":quit" や ":close" で済むのでこれを明示的に使うこと自体少ないとは思うんだけど ) が buffer に割り当てられた memory を解放するだけなのに対して他 2 つは vim 内の buffer に関する metadata に対して影響を及ぼすみたい。 ":bdelete" は buffer list から該当 buffer に関する情報を消すってことで ":bwipeout" は mark や変更履歴など他すべての該当 buffer に関する情報を消去するってことらしい。うーん意識的に使うとしたら ":bdelete" くらいかなぁ。てか ":bwipeout" はなんか犯罪臭がする、のはおれはよこしまだからですね。あーここらへんと関連して 'bufhidden' はこの 3 つに対応する "unload", "delete", "wipe" とあと "hide" が設定できるらしい。該当 buffer がどの window にも表示されなくなる場合 ( その buffer が表示されている最後の window で ":quit", ":close" したとき ) の振る舞いを指定するわけだ。しっくりきた。
    • てか ":buffers" や ":badd", ":bdelete" etc の記述が "Argument and buffer list commands" ( :help buffer-list ) じゃなくて "Using hidden buffers" の項 ( :help buffer-hidden ) にあるのは何でだ。これ windows.txt ( buffer や window の記述がしてある help file ) を最初から読んでくならともかく最初の目次ぽい tag が並んでるところだけ見ると作為的な気がする…。
    • 特殊な buffer ( :help special-buffers ) のところの scratch buffer てのは便利そうだなぁと思うんだけど実際使うとめんどくさいとちょっと思った。てのも空の buffer は問答無用で scratch にする autocmd を打とうかと思ったんだけどいざいろいろ書いてみて保存したい ( ":saveas" ) と思ったときに scratch じゃなくする autocmd も定義しないといけないんだよね。あとここで記述されてる scratch buffer ては :set buftype=hide するわけだからこの buffer を何度も使うという前提、たよね ? てことはおれみたいにとにかく scratch! て場合は :set buftype=wipe にして使い捨て万歳にしておけばいいのかなーとか。
    • どうでもいいけど ":blast" が何か打ち出したり ":ball" で丸いのが出てきたり :sunhide で日蝕がおこったりするのかと思ってしまうよな。
  • window
    • :help windows
    • help は buffer と同じ位置に書かれている。しょっぱなの説明に buffer の上にのった表示領域 と書かれているけどこれはつまり buffer の一部分を表示するための「窓」ということだね。
    • help を読んでいるとまず window の開き方について説明されるんだけど command の名前や説明を見てる限りでは window の分割というのが主旨みたい ( split という単語が非常に多い ) 。つまり既存 window を分割することが新しく window を作ることなんだよと暗示している ? てか分割関係の (ex|normal) command がすごいいっぱいあるんだけどほとんどが同じ操作に対する synonym というところを見ると vim は ( というか設計者である Bram は ) ここらへんの操作をかなり重要に捉えているということかな。
    • でも window って monitor size が大きくない限りそんなにいくつも開けないよね…。何か参照しながら何か書くのに便利っていうのはわかるんだけどそれも同時に 2 つの window が限界というかそれ以上開くとごちゃごちゃしてしまって逆にやりづらいというか。そもそも視点移動だけで見ることが可能じゃないとダメっていう状況はそんなにないと思うんだけどあんまり大規模な開発やったことないからかなぁ。
    • あー preview-window ってのが効いてくるのかな。 C やその後継言語だと header file として関数宣言を分割して書けるのでその interface が一発でわかるとかそれでなくても最近の scripting language で sub routine の定義を確認するのに使えるってのは大きいかも。てか Microsoft 社の提供する intellisence ( Visual Studio の editor で code 打ってると関数定義を tooltip で表示してくれる ) に比べるとけっこう無骨な感じなんだけど打ってる最中で keybind が変化することがないし直接定義部分をのぞけるというのは確実性においてはこれ以上ない選択肢だ。まぁ intellisence は intellisence で library に対する知識が全くなくても ( というか .NET Framework の library の一部は source すら付属してないんだけどそれでも ) 支障なく code を打てるほどの omni 補完がついているので一長一短というところかな。
    • ":quit" と ":close" はそれぞれ「 window もしくは vim を終了する」と「 window を閉じる」という位置づけなのかな。というのも ( help でない ) window がひとつのみある状態でそれぞれ打つと前者は vim を終了するのに対して後者は error が出るからそう感じたということなんだけど。他の振る舞いは変わらないみたいなので vim を閉じたくない意識がある場合は ":close" を使えば安心ということかな。
  • tabpage
    • :help tabpage
    • tabpage はひとつ以上の window を包含すると書いてあるので window を持たない tabpage は存在しないということか。そして "tab" ではなくて "tabpage" なのは単純に tab space ( CHARACTER TABULATION ) と区別するため、だよなたぶん。
    • 唯一の tabpage local な option が 'cmdheight' らしいんだけどこれは、んー command の実行結果が複数行に及ぶことが予想できる tabpage は多めにcommand line を取っとくとか ? っていうか :help 'cmdheight' の記述に tabpage local っていう表示がないな。まぁそのうち修正されるか。
    • :help tab-page-other に載ってる新しく tabpage を開く場合の autocmd event の発生順 ( WinLeave, TabLeave, TabEnter, WinEnter, BufLeave, BufEnter ) を見ると buffer の扱いがけっこう特殊な感じ ? ではないな、やっぱり window に比べると tabpage が特殊なんだ。 window には file を指定して開くという command がないにも関わらず ( 新しい window を作るにはもっぱら split するしかない ) その外側の概念である tabpage にはそれ ( ":tabedit", ":tabnew" ) がある、というところから推測すると既存 window を分割したもの ( この window は current buffer を覗いている ) を新しい tabpage に所属させたあとにその buffer を空にする、という流れなのかな ? ( help comment にはこういう処理「のようなこと」をしていると書いてあるけど実際にやってるぽい ? ) 対してすでに開かれている tabpage へうつるときには自然な形 ( BufLeave, WinLeave, TabLeave, TabEnter, WinEnter, BufEnter ) なのでうーんこれ設計が…、あーうん、まぁ file に近い順番に挙げていくと buffer, window, tabpage の順番だと思うんだけど一番遠いところだけが直接 file に access できる path を持っているということからくる矛盾だと思うんだけど。 vi との互換や歴史を考えると仕方のないことなのかな。
    • :help tab-page-other に載ってる新しく tabpage を開く場合の autocmd event の発生順 ( WinLeave, TabLeave, TabEnter, WinEnter, BufLeave, BufEnter ) を見ると buffer の扱いがけっこう特殊な感じ ? ではないな、 window が特殊なんだ。 window にも file を指定して開くという command があるんだけど ( ":new", ":vnew" ) その際の振る舞いも似たような感じになっていて ( WinLeave, WinEnter, BufLeave, BufEnter ) 上記の場合の処理はそれに tabpage の分を追加したものになっている。具体的には多分既存 window を分割したもの ( この window は current buffer を覗いている ) を新しい tabpage に所属させたあとにその buffer を空にする、という流れなのかな ? ( help comment にはこういう処理「のようなこと」をしていると書いてあるけど実際にやってるぽい ? ) 対してすでに開かれている tabpage へうつるときには自然な形 ( BufLeave, WinLeave, TabLeave, TabEnter, WinEnter, BufEnter ) なのでうーんこれ設計が…、あーうん、多分 window の基本的な振る舞いとして分割しかできないというのがあるんだろうけどこれがそもそもの原因というか誤解の元のような気がする。
    • あとはー 'tabline' の設定をする sample code が載ってるのがけっこう point 高い ( :help setting-tabline ) 。標準の 'tabline' だとけっこう手狭になるんよね。ということで以下のような感じに変更。ぶっちゃけ vimp と同じで icon だけでいいやと思ってたんだけど無理な相談なので拡張子と filetype を表示するようにした。 code は brush up する予定。
" see :help setting-tabline
set tabline=%!MyTabLine()
function! MyTabLine()
    let s = ''
    for i in range(tabpagenr('$'))
        " select the highlighting
        if i + 1 == tabpagenr()
            let s .= '%#TabLineSel#'
        else
            let s .= '%#TabLine#'
        endif

        " the label is made by MyTabLabel()
        let s .= ' %{MyTabLabel(' . (i + 1) . ')} '
    endfor

    " after the last tab fill with TabLineFill and reset tab page nr
    let s .= '%#TabLineFill#%T'

    return s
endfunction

function! MyTabLabel(n)
    let buflist = tabpagebuflist(a:n)
    let numofbuf = len(buflist)
    let winnr = tabpagewinnr(a:n)
    let bufnr = buflist[winnr - 1]
    let ext = fnamemodify(bufname(bufnr), ':e')

    let result = ''
    if numofbuf != 1
        let result .= numofbuf . ' '
    endif
    let result .= (ext == '') ? '-' : ext
    let result .= '[' . getbufvar(bufnr, '&filetype') . ']'
    return result
endfunction