読者です 読者をやめる 読者になる 読者になる

statusline系プラグイン第4の刺客 vim-ezbar

この記事はVim Advent Calendar 2013 40日目の記事です。

statuslineをモダンに改造するプラグインの系譜

そして第4の刺客として、t9md/vim-ezbar が登場しました。
以下、特徴と利用法を解説します。

※ezbarの仕様が変わりました。そのうちこの記事を書き直すかも

[特徴]

lightline.vimとの比較*1、細かく説明すると長くなるので簡潔に述べます。
利点:

  • よりシンプルに設定を記述可能
  • 文脈に合わせての色変更がやりやすい(gitのブランチがmasterでない時は色を変更するなど)
  • 文脈に応じて表示する部品を削除できる(特定プラグインを利用している時にはそれ用の部品以外は表示させないなど)
  • グローバル関数を用意しなくてもいい
  • ctrlp.vimと競合しない

欠点:

  • デフォルト設定はない(設定は作らないといけない)
  • タブライン機能はない
  • カラーテーマはない(基本、色は自分が一から指定する)
  • 部品と色設定との結合が強い
  • 文字列を返すだけの部品ではお手軽さで劣る

向いている人:

  • 色から表示する内容まで細かく指定したい人
  • 簡潔な記述を望む人
  • 文脈に応じてラインの内容を変更したい人

向いていない人:

  • 設定抜きにすぐに使い始めたい人
  • プラグインをなるべくデフォルトで使って満足できる人
  • 複数のカラーテーマを着せ替え気分で使い分けたい人
  • タブラインを利用していて、ステータスラインと同じプラグインタブラインも設定したい人

[実践]

以前 lightline.vimをカスタマイズする という記事を公開しました。そこで作ったステータスラインとほぼ同じ物を vim-ezbar で実現してみます(一部簡略化します)。

let g:ezbar = {'separator_L': '', 'separator_R': ''}

まずこのようにg:ezbar辞書を定義します。separator_L separator_R は部品を区切る区切り文字です。デフォルトでは|で区切られますが、境界をなくしたいので空文字を指定します。

(レイアウト)

どの部品をどのように並べるのかを決めます。
標準で用意されている部品や、新しく部品を登録する方法は後述します。
ちなみにlightline.vimでは部品のことをコンポネントと呼びましたが、ezbarではパートと呼びます。
パートのレイアウトを決めるには2種類のリスト変数g:ezbar.activeg:ezbar.inactive を使います。
g:ezbar.active が現在アクティブなステータスラインに表示される内容。
g:ezbar.inactive非アクティブなステータスラインに表示される内容です。

これらのリストに利用したいパートの名前の文字列を並べていきます。
{'chg_color': <color>}{'__SEP__': <color>} という特殊なパートがあります。
この<color>の部分 にはハイライトグループ名(文字列)を指定するか、

{'gui': [guibg, guifg, gui], 'cterm': [ctermbg, ctermfg, cterm] }

という形の辞書を指定します。この辞書の要素の 'cterm' などは省略可能です。
例えば{'gui': ['green', 'white']} だと、:highlight guibg=green guifg=white になります。

この特殊なパート{'chg_color': <color>}は、挿入した場所から先の g:ezbar.parts.__default_color(色が指定されていないとき使われる色)*2 を変更します。
{'__SEP__': <color>}はstatusline の左側と右側を分け隔てる区切りです。そしてその区切りの色は、指定したものになります。

次のg:ezbar.activeは私が使っているものです。

let g:ezbar.active = [
  \ 'winbufnum',
  \ 'dir',
  \ 'filename',
  \ {'chg_color': {'gui': ['SlateGray', 'white', 'bold']}},
  \ 'filetype',
  \ 'modified',
  \ 'currentfuncrow',
  \ {'__SEP__': 'StatusLine'},
  \ 'cfi',
  \ 'fileformat',
  \ 'encoding',
  \ 'percent',
  \ 'line_col',
  \ ]

途中でchg_color されているので、g:ezbar.parts.__default_colorが変更され、この後のパート('filetype' や 'modified'など)はgui背景色がSlateGray 文字色が白で太字で表示されます。
'currentfuncrow' の後に __SEP__ があるので、ここでステータスラインの左右が分かたれ、間はハイライトグループStatusLineで埋められます。

let g:ezbar.inactive = [
  \ 'winbufnum',
  \ 'dir',
  \ 'filename',
  \ {'chg_color': {'gui': ['SlateGray', 'white']}},
  \ 'filetype',
  \ 'modified',
  \ {'__SEP__': 'StatusLine'},
  \ 'encoding',
  \ 'percent',
  \ 'line_col',
  \ ]

このようにezbarはパートという部品を列挙するだけで簡単にステータスラインを作成できます。
標準のパートは以下のものです。(標準のパートを使うためにはそのための関数を呼ぶ必要があります)

パート 説明
mode ノーマルモードやインサートモードなど、モードの状態を表示
percent 現在バッファの上から何パーセントの場所にいるか表示(なぜかアクティブ時には色が付く)
modified &modifiedされているか
readonly 読み込み専用か
line_col 行と列を表示
line 現在行/総行数
encoding バッファのエンコード
fileformat バッファのfileformat
filetype バッファのfiletype
filename バッファのファイル名
winnr ウィンドウの番号

残りのパートは自分で作って用意します。

(パートの用意)

パートはg:ezbar.partsに登録して用意します。
部品を作るに当たり、他のライン系プラグインと違って、できないことがあります。ですが回避手段も用意されています。
以下のことが出来ません。

  • g:ezbar.partsに登録するのは全て関数です。複雑な処理をせず、ただ文字列だけのパートであっても、一度関数を作ってそれに文字列を返させるという冗長なことをする必要があります。
  • 'statusline' に渡す文字列に %{} (実行されるときの文脈で'%{'と'}'の間の expression を評価し結果に置き換える)を使うことが出来ません
    • 当然%{expand('%')} で現在の文脈のバッファ名を得るなんてことも出来ません。
    • 当然%{winnr()} で現在の文脈のウィンドウ番号を得ることも出来ません。
    • ウィンドウローカル変数やバッファローカル変数%{w:varname} %{b:varname} で参照することも出来ません。

%{} で文脈を得ることの代替として、パート関数には引数に "現在評価中のステータスラインのあるウィンドウの番号" が渡されます。このウィンドウ番号を文脈として、必要な情報を作るようにします。

  • 現在の文脈のバッファ名はbufname(winbufnr(a:n))で得るようにします。
  • ウィンドウ番号は与えられます。
  • 現在の文脈のウィンドウローカル変数やバッファローカル変数getwinvar(a:n, 'varname') getbufvar(winbufnr(a:n), 'varname') で得るようにします。

こういうことをしないと文脈情報を得られなくなったので、冗長になったように思えますが、しかし逆に文脈を取得した後は、関数内でその文脈を使って複雑な処理をすることが可能になりました。つまりこの冗長さはezbarのパワフルさとトレードオフです。
しかもこの文脈をg:ezbar.parts._init()で一度 self 変数に登録してしまえば、以後はどの部品からでも登録した文脈を利用することが出来るので益の方が大きくなります。g:ezbar.parts._init()はステータスラインの評価の開始時に一番始めに実行される関数です。

g:ezbar.partsは、パートを収めた辞書ですが、その中で "_init" と "_filter"*3 というパートは特別な役割を果たします。
g:ezbar.parts._init() は、パート関数の中で一番始めに実行されてこれから各パートで使う変数を用意したり、ステータスラインの状態を変えたりします。
g:ezbar.parts._filter()は、パート関数の中で一番最後に実行されて、各パートのプロパティを書き換えたり、パートそのものをなかったことにすることができます。

では"_init"から"_filter"まで、各パートの中身を書くことにします。
なお、ここではs:uという変数を用意し、最後にs:us:ezbar.partsに統合する方針を採るので、これから出てくるs:uは、最終的にはs:ezbar.partsに統合されると考えて下さい。

let s:u = {}
function! s:u._init(n)
  let self.bufname = bufname(winbufnr(a:n))
  let self.mode = mode()
  if self.__is_active && self.mode==#'i'    "アクティブでインサートモードの時、デフォルト色変更
    let self.__default_color = {'gui': ['DarkKhaki', 'black', 'bold']}
  end
endfunction

前述の通りg:ezbar.parts._init()に渡される引数は評価中ステータスラインのウィンドウ番号です。
現在の文脈のバッファをself.bufnameに代入します。これでこれ以降のパートではself.bufnameでバッファ名を得られるようになりました。
他に、現在のモードによって表示を変更するというのを複数のパートでやりたいのでmode()の結果もself.modeに代入します。

また、self.__is_active は評価中のステータスラインはアクティブかどうかが代入されます。self.__default_colorは、特に色を指定していないパートはこの色になるようにします。評価開始時にはStatusLine StatusLineNCの色がデフォルトになっていますが、途中で変更することが出来ます。変更すると以降のパートでは変更した色が利用されます。
ezbarで使われる色はハイライトグループ名か、{'gui': [guibg, guifg, gui], 'cterm': [ctermbg, ctermfg, cterm] }の形式で指定します。

さて、_init()で準備は整えたのでいよいよパートを作ります。まずはバッファ番号とウィンドウ番号を表す"winbufnum"です。

function! s:u.winbufnum(n)
  return '%n%{repeat(",", winnr())}%<'
endfunction

返り値である文字列がパートになります。

次に現在ウィンドウに表示中のバッファのあるディレクトリを返す"dir"です。

function! s:u.dir(n)
  let bg = self.mode==#'i' ? 'LightSkyBlue1': 'azure' "インサートモードの時文字色変更
  return {'s': '%.35('. fnamemodify(self.bufname, ':h'). '%)',
    \ 'ac': {'gui': [bg, 'black', 'bold']}, 'ic': {'gui': ['azure', 'black']}}
endfunction

"dir" のパート関数で _init() で定義しておいたself.modeself.bufnameを利用しています。
返り値は文字列ではなく辞書を使っています。そうした場合、キー"s" の文字列がパート本体になり、キー"ac"、 "ic"、 "c"でアクティブ時の色、非アクティブ時の色、デフォルトの色を指定することが出来ます。

次の部品は 'filetype' が "vim" のとき、現在カーソル位置が、関数の始まりから何行目なのかを表すものです。

  function! s:u.currentfuncrow(n)
    if &ft != 'vim'
      return ''
    end
    let funcbgn = search('^\s*\<fu\%[nction]\>', 'bcnW', search('^\s*\<endf\%[unction]\>', 'bcnW'))
    if funcbgn > 0
      let row = line('.') - funcbgn
      return row ? {'s': row, 'c': {'gui': ['azure', 'black', 'bold']}} : ''
    endif
    return ''
  endfunction

条件に合わないときには空文字を返しています。パート関数の返り値がempty()だった場合、g:ezbar.parts._filter()が呼ばれる前に除去されて存在しなかったことになります。
こうして文脈に応じてパート自身が自分の色を決めたり、自分が存在するかどうかを決めることが可能です。
g:ezbar.parts._filter()でも色を変更したり、除去したり、出力される文字列を変更したり出来ます。だから気に入らない色は最終的に修正することは可能です。
ただ、g:ezbar.parts._filter()での処理は若干煩雑になるので、出来るならパーツ内で処理を完結させたほうがいいでしょう。

では、tyru/current-func-info.vimという、現在のカーソル位置が関数内にあるときに関数名を返してくれるプラグインがありますが、それを利用する部品を作ります。

function! s:u.cfi(n)
  if exists('*cfi#format')
    return {'s': cfi#format('%.43s()', ''), 'c': {'gui': ['azure', 'black', 'bold']}}
  end
  return ''
endfunction

これで自分で作るパートは"_filter"を除いて揃いました。最後に標準で用意されているパートを統合します。*4

call extend(s:u, ezbar#parts#default#new(), 'keep')

(最終処理)

g:ezbar.parts._filter()で最終処理を行います。
自分で作ったパートはすでに色を設定したり文脈に応じた処理を施しましたが、標準で用意されているパートには何の手も加えていません。
そこで、最後にカスタマイズします。
g:ezbar.parts._filter()に渡される引数は今までのパートのものと違います。
引数は2つ。各パートをg:ezbar.active g:ezbar.inactiveの順に並べたリストである layout と、各パートを収めた辞書 parts です。
全てのパートは辞書化されています。文字列で返したパートも辞書化されて、キー"c" には、そのときのself.__default_colorがセットされています。また、新しく "name" というキーも作られ、それにパート名が収められています。
リストlayoutと、辞書parts、どちらにも要素にはパートが収められていて同じ物を参照しています。a:layout[0]a:parts.winbufnum でどちらでも"winbufnum"パートにアクセスできます。
これの使い分けはpartsが個のパートを直接修正するときに利用し、layout特定のパートを除去するのに使います。各パートに追加されたキー"name"はパートをフィルタリングするのに使えます。(しかし今回は使いません。)
処理を追えたら最後にlayoutを返します。

function! s:u._filter(layout, parts)
  if self.mode == 'i'
    let a:parts.__SEP__.ac = {'gui': ['DarkKhaki', 'black', 'bold']}
  end
  if has_key(a:parts, 'filename')
    let a:parts.filename.ac = {'gui': [(self.mode==#'i' ? 'RosyBrown1': 'MistyRose'), 'black', 'bold']}
    let a:parts.filename.ic = {'gui': ['MistyRose', 'black']}
  end
  call extend(a:parts.percent, {'ac': {'gui': ['MistyRose', 'black', 'bold']}, 'c': {'gui': ['MistyRose', 'black']}})
  call extend(a:parts.line_col, {'ac': {'gui': ['NavajoWhite1', 'black', 'bold']}, 'c': {'gui': ['NavajoWhite1', 'black']}})
  return a:layout
endfunction

2行目でa:parts.__SEP__にキー"ac" を追加しています。__SEP__もパートです。*5
インサートモードの時、アクティブウィンドウのステータスラインのセパレータ色を変更するようにしました。
4行目でfilenameがあるかどうかを確認しているのは、無名バッファを開いたときにはfilenameは空文字になるため、除去されて存在しなくなるからです。
今回は色の変更にしか使っていませんが、g:ezbar.parts._filter()では文脈に応じて高度なことも出来るようです。

(完成)

s:ug:ezbar.partsに代入して完成です。

let g:ezbar.parts = s:u
unlet s:u

f:id:leafcage:20140109172118p:plain

ミニマリスト向けと銘打っているとおり、シンプルな設定でステータスラインを実現できました。

[アフターケア]

どこかの表記がおかしくてエラーが発生すると抜けられなくなります。そんなときには:EzBarDisableコマンドでezbarを無効にしてから対処しましょう。

色の選択には vim-ezbar/misc/colortest/compact.vimvim-ezbar/misc/colortest/full.vim を開いて :so %すると、とても見やすい色一覧が表示されます(素晴らしいです。ezbar以外でも利用できそうですね)。

f:id:leafcage:20140109172212p:plain

vim-ezbar/README-JP.mdに制作者による解説が、
vim-ezbar/misc/config_sampleに設定例が掲載されています。

*1:他のline系プラグインは使ったことがないので

*2:標準では__default_colorにはアクティブウィンドウでは StatusLine 非アクティブウィンドウでは StatusLineNC のハイライトグループが使わます

*3:将来的に_finishという名前に変更される可能性があるそうです

*4:もし全ての部品を自前で用意するのなら、この処理は必要ありません

*5:'chg_color'はパートではありません