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.active
と g: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:u
をs: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.mode
と self.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:u
をg:ezbar.parts
に代入して完成です。
let g:ezbar.parts = s:u unlet s:u
ミニマリスト向けと銘打っているとおり、シンプルな設定でステータスラインを実現できました。
[アフターケア]
どこかの表記がおかしくてエラーが発生すると抜けられなくなります。そんなときには:EzBarDisable
コマンドでezbarを無効にしてから対処しましょう。
色の選択には vim-ezbar/misc/colortest/compact.vim
や vim-ezbar/misc/colortest/full.vim
を開いて :so %
すると、とても見やすい色一覧が表示されます(素晴らしいです。ezbar以外でも利用できそうですね)。
vim-ezbar/README-JP.mdに制作者による解説が、
vim-ezbar/misc/config_sampleに設定例が掲載されています。