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

作者が教える! lightline.vimの設定方法! 〜 中級編 - 展開コンポーネントを理解しよう

Vim

この記事では、lightline.vimの中で特に難しいcomponent_expandの使い方を教えます。これまでの記事を並べておきます. おさらいされる方はどうぞ。

コンポーネントは三種類ある

これまでの流れでご察しの通り、コンポーネントにはそのまま指定するものと、関数を指定するものの二つがあります。

component_functionでできることは、componentで出来ます。component_visible_conditionを適切に指定してやれば、全く同じ挙動になります。逆に、componentにできてもcomponent_functionに出来ないことはあるので、真に包含関係が成立しています。

長い間この二つで十分だと思っていたのですが、ある都合によりもう一種類のコンポーネントが追加されました。

実は、component_expandでできることは、componentにもcomponent_functionにも出来ません。少し特別なのです。

lightline.vimの開発の中で後付されたということもあって、これがある意義がよく分からないとか、設定の仕方がよく分からなかったり、そもそも存在すら知らなかったりすると思います。そこで、ここでは展開コンポーネントの使い方を徹底的に説明したいと思います。

syntasticと連携させる

例題から入りましょう。syntasticと言うのは、Vimプラグイン界では有名なシンタックスチェッカーの一つです。このsyntasticのステータスを、ステータスラインに表示することを考えます。syntasticでは、SyntasticStatuslineFlagという、ステータスラインに表示することを意図とした関数が用意されています。

let g:lightline = {
      \ 'active': {
      \   'right': [ [ 'lineinfo',  'syntastic' ],
      \              [ 'percent' ],
      \              [ 'fileformat', 'fileencoding', 'filetype' ] ]
      \ },
      \ 'component_function': {
      \   'syntastic': 'SyntasticStatuslineFlag'
      \ }
      \ }

component_functionに登録するだけです。簡単ですね。しかし

うーむ... エラーなのに見にくい...

やはりエラーは目立って欲しいですよね。こんな時、component_expandを使います。とりあえず、こんなふうに書いて下さい。

let g:lightline = {
      \ 'active': {
      \   'right': [ [ 'syntastic', 'lineinfo' ],
      \              [ 'percent' ],
      \              [ 'fileformat', 'fileencoding', 'filetype' ] ]
      \ },
      \ 'component_expand': {
      \   'syntastic': 'SyntasticStatuslineFlag'
      \ },
      \ 'component_type': {
      \   'syntastic': 'error'
      \ }
      \ }


あれ?syntasticは動いているのに何も出ない?

次のコマンドを叩いて下さい。

:call lightline#update()


出ました! syntasticのエラーが真っ赤になって出ました!

保存すると同時にsyntasticを動かし、かつlightline#updateを呼ぶと便利です。syntasticはデフォルト設定では勝手に動いてしまうので、passiveにしてこちら側でチェックするようにしてやります。ここらへんは、ファイルタイプに応じて適当にやって下さい。

let g:lightline = {
      \ 'active': {
      \   'right': [ [ 'syntastic', 'lineinfo' ],
      \              [ 'percent' ],
      \              [ 'fileformat', 'fileencoding', 'filetype' ] ]
      \ },
      \ 'component_expand': {
      \   'syntastic': 'SyntasticStatuslineFlag',
      \ },
      \ 'component_type': {
      \   'syntastic': 'error',
      \ }
      \ }
let g:syntastic_mode_map = { 'mode': 'passive' }
augroup AutoSyntastic
  autocmd!
  autocmd BufWritePost *.c,*.cpp call s:syntastic()
augroup END
function! s:syntastic()
  SyntasticCheck
  call lightline#update()
endfunction


自動でsyntasticのエラーが出るようになりました! ばんざーい!ヽ(´ー`)ノ

component_expandとは

不思議でしょう?この機構を考えるのに一日かかりましたから.最初に思いついた時は、嬉しさが体の芯から滲み出る感じがしましたね。

絶対的に守られるべき原則として、「コンポーネント集合の中のコンポーネントは全て同じ色」というものがあります。(コンポーネントコンポーネント集合の用語が分からない人は、作者が教える! lightline.vimの設定方法! 〜 初級編 - コンポーネントを作ってみよう - プログラムモグモグを先に読んで下さい。) componentでユーザーが直接ハイライトグループを指定しない限り、この原則は守られます。その中で、syntasticのようなエラーメッセージを赤くするにはどうすればいいのでしょうか。

そうです. syntasticを一つのコンポーネント集合に昇格すればいいのです. もう一度、設定を見てみます。

      \ 'active': {
      \   'right': [ [ 'syntastic', 'lineinfo' ],                     " 色付け 0番
      \              [ 'percent' ],                                   " 色付け 1番
      \              [ 'fileformat', 'fileencoding', 'filetype' ] ]   " 色付け 2番
      \ },

これに対して、lightline.vimはステータスラインにセットする前に「展開」します。つまり、component_expandに設定されている関数は先に評価されます。その結果、コンポーネント

      \ 'active': {
      \   'right': [ [ '[Syntax: line:1 (1)]' ],                      " 展開された! component_typeはerror
      \              [ 'lineinfo' ],                                  " 色付け 0番
      \              [ 'percent' ],                                   " 色付け 1番
      \              [ 'fileformat', 'fileencoding', 'filetype' ] ]   " 色付け 2番
      \ },

のようになります (イメージです)。展開されたsyntasticの結果は、もはや関数ではありません。ただの文字列(関数の返り値)です。

そして、errorコンポーネント集合に対しては、カラースキームが用意しているerror用の色が使われるのです。lightline.vimが用意しているカラースキームは、展開コンポーネント用に「error」と「warning」の色を用意しています。カラースキームを自作される方は、これに従って下さい。

展開のイメージを持っていただくために、もう一つ例を挙げます。次の設定はどうでしょう?

let g:lightline = {
      \ 'active': {
      \   'right': [ [ 'lineinfo' ],
      \              [ 'percent' ],
      \              [ 'fileformat', 'syntastic', 'fileencoding', 'filetype' ] ]
      \ },
      \ 'component_expand': {
      \   'syntastic': 'SyntasticStatuslineFlag',
      \ },
      \ 'component_type': {
      \   'syntastic': 'error',
      \ }
      \ }


予測出来ましたか?

まず、最初は

      \ 'active': {
      \   'right': [ [ 'lineinfo' ],                                              " 色付け 0番
      \              [ 'percent' ],                                               " 色付け 1番
      \              [ 'fileformat', 'syntastic', 'fileencoding', 'filetype' ] ]  " 色付け 2番
      \ },

となっています。ここで、component_expandに登録されているsyntasticコンポーネントを展開します。展開されたコンポーネントは、コンポーネント集合に昇格します。そうすると2番目のコンポーネント集合がぶっ千切れます。

      \ 'active': {
      \   'right': [ [ 'lineinfo' ],                                              " 色付け 0番
      \              [ 'percent' ],                                               " 色付け 1番
      \              [ 'fileformat' ]                                             " 色付け 2番
      \              [ '[Syntax: line:1 (1)]' ]                                   " 展開された!
      \              [ 'fileencoding', 'filetype' ] ]                             " 色付け 2番
      \ },

右のコンポーネント集合は右から並ぶことに注意してください (ただし、コンポーネント集合の中では左から並びます)。ややこしいですが、そうなっているのです (この挙動に関しては少し設計を間違えたかもという気持ちはありますが、影響範囲があまりにも大きいので、挙動を変更するつもりはありません)。よって、目に見える順番にすると

[ [ 'fileencoding', 'filetype' ], [ '[Syntax: line:1 (1)]' ], [ 'fileformat' ], [ 'percent' ], [ 'lineinfo' ] ]

となります。

きちんと合っていますね。いやいや、合っていますねといえど、'fileformat'コンポーネントがあっちに行ったりこっちに行ったりしてしまいます。これはどうしようもありません。右から並べるならそれで統一すれば良かったのですが、もう多くの人が自分の設定を書いていますので、いきなり逆にすると大混乱になりそうです。それよりも、次のルールを守っていただくほうが、影響も少なくてすみます。即ち、「展開コンポーネントコンポーネント集合の端に登録して下さい」。

かっこいいセパレーターが設定されてる時も、適切に処理されます。


作っている時に、この完成図をイメージすると、色を変えるにはコンポーネント集合に昇格させなければならないと分かったわけです。

なぜlightline#update()を呼ばなくてはならないのか

これを説明するには、lightline.vim

  • どのタイミングで
  • 何を
  • どのオプションに

設定しているかを説明しなくてはいけません。

lightline.vimは、アクティブなウィンドウとそうでないウィンドウは、別のステータスラインを設定します。つまり、ウィンドウの移動するタイミングでは、アップデートしなくてはいけません。このとき、lightline#update()が呼ばれます。

この関数は、見えているウィンドウに対してステータスラインをセットする関数です。

正確に言うと、「各ウィンドウの&statuslineにlightline#statusline(inactive)の返り値を」セットします。展開コンポーネントは、lightline#statusline関数の返り値の中では既に展開されているのです。つまり、展開コンポーネントであるsyntasticは、関数SyntasticStatuslineFlagの結果、すなわち'[Syntax: line:1 (1)]'という文字列が、&statuslineにセットされるのです。関数コンポーネントは、関数名が&statuslineに登録されます。関数名をFとしますと、%( %{exists("*F") ? F() : ""} %)のように登録されます。このFは、カーソルが動くたびに何度も何度も呼ばれ、勝手に更新されるのです。

展開コンポーネントは、&statuslineに返り値がセットされます。結果が文字列としてセットされます。カーソルが動いたところで、何も変わりません。(これを用いて関数が呼ばれる回数を減らし、パフォーマンスを上げることも出来ます)

syntasticでチェックを行われるのは、ファイルが保存されるタイミングです (もちろん手動でもチェックできます)。このイベントでは、lightline.vimはlightline#update()を呼びません。だから、展開コンポーネントである'syntastic'コンポーネントをアップデートするためには、こちらがlightline#update()を呼ばなくてはいけないのです。

ややこしいですが、こうなっているのにも理由があります。簡単な理由です。コンポーネント集合の結果が全て空文字列でも、幅が潰れません。

こういうことです。しかし、syntasticのコンポーネントは真っ赤です。

不格好ですよね...

全てが空文字列のコンポーネント集合を自動的に、つまり&statuslineにセットした後で隠すのは、とても難しいタスクです。ならば、&statuslineにセットする前に展開されるコンポーネントがあってもいいのでは?ということで作られたのが、展開コンポーネントなのです。先に関数を評価し、展開しておけば、空文字かどうかでコンポーネントを隠すことは出来ます。

tablineはどうなっているのか

GVimをお使いの人は気付いていないと思いますが、lightline.vimはtablineもサポートしています。

もちろんtablineもコンポーネントで作られていますし、ユーザーが設定できるようになっているのですが、それは次回か次々回に回しましょう。statuslineとtablineは、コンポーネントの源を共有しています。つまり、大体同じ感じで設定することが出来ます。tablineの設定は、デフォルトはこうなっています。

let g:lightline.tabline = {
      \ 'left': [ [ 'tabs' ] ],
      \ 'right': [ [ 'close' ] ] }

この、tabsコンポーネント、これはまさに展開コンポーネントです。実際、tabsコンポーネントはどのように設定されているかというと

let g:lightline.component_expand = {
      \ 'tabs': 'lightline#tabs' }
let g:lightline.component_type = {
      \ 'tabs': 'tabsel' }

となっています。
展開コンポーネントはこのtabsコンポーネントのためにあるのです。
syntasticなどにも使えるというのはおまけみたいなものです。

さて、適当に3つくらいタブを開いて、lightline#tabs()関数の返す値を見てみましょう。

3つの要素の配列が返されています。

  [ ['%1T%{lightline#onetab(1,0)}'],
    ['%2T%{lightline#onetab(2,1)}'],
    ['%3T%{lightline#onetab(3,0)}%T'] ]

よく見ると、各要素も配列で、その要素が文字列となっています。タブが増えるとこうなります。

  [ ['%1T%{lightline#onetab(1,0)}', '%2T%{lightline#onetab(2,0)}'],
    ['%3T%{lightline#onetab(3,1)}'],
    ['%4T%{lightline#onetab(4,0)}','%5T%{lightline#onetab(5,0)}%T'] ]

真ん中が現在のタブです。

一般論で話します。展開コンポーネントに設定された関数は、文字列を返す代わりに、3つの要素の配列を返しても構いません。例えば、コンポーネントの構成が

[ [ 'a', 'b', 'expand', 'c', 'd' ],    " index 0
  [ 'e' ],                             " index 1
  [ 'f' ] ]                            " index 2

となっていたとします。
'expand'が展開コンポーネントです。
まず、この展開コンポーネントが、文字列'EXPAND'を返したとしましょう。展開された結果は次のようになります (これは今までに説明してきました)。

[ [ 'a', 'b' ],                        " index 0
  [ 'EXPAND' ],                        " 展開された!!! ここが特別の色付け!!!
  [ 'c', 'd' ],                        " index 0
  [ 'e' ],                             " index 1
  [ 'f' ] ]                            " index 2

次に、展開コンポーネントが次の配列を返したとします。

[ [ 'EXPAND 0', 'EXPAND 1' ],
  [ 'EXPAND 2', 'EXPAND 3' ],
  [ 'EXPAND 4', 'EXPAND 5' ] ]

そうすると、展開された結果は次のようになります。

[ [ 'a', 'b', 'EXPAND 0', 'EXPAND 1' ],      " index 0
  [ 'EXPAND 2', 'EXPAND 3' ],                " 展開された!!! ここが特別の色付け!!!
  [ 'EXPAND 4', 'EXPAND 5', 'c', 'd' ],      " index 0
  [ 'e' ],                                   " index 1
  [ 'f' ] ]                                  " index 2

tablineの例に戻りましょう。展開される前のコンポーネントの並びは、次のようになっていました。

[ [ 'tabs' ] ]

展開した結果は、次のようになります。

[ ['%1T%{lightline#onetab(1,0)}', '%2T%{lightline#onetab(2,0)}'],    " index 0
  ['%3T%{lightline#onetab(3,1)}'],                                   " 展開された! 色付けはtabsel
  ['%4T%{lightline#onetab(4,0)}','%5T%{lightline#onetab(5,0)}%T'] ]  " index 0

だから、選択タブの左側と右側で同じ色付けがされるのです。このようにして、タブは作られているのです。

現在のタブを一つのコンポーネントにすることが、アルゴリズムをシンプルにするという確信がありました。最初は、色々ハードコーディングもしてみました。その中で、コンポネントを先に展開して処理すると言う形になったのです。展開コンポーネントは、tabsコンポーネントのために作られました。実は、statuslineもtablineも全く同じ処理の関数s:lineで作られています。

function! lightline#statusline(inactive)
  return s:line(0, a:inactive)
endfunction

function! lightline#tabline()
  return s:line(1, 0)
endfunction

Minimalismはlightline.vimが掲げる理念の一番ですから。
展開コンポーネントを理解していただくことは、lightline.vimの処理とより親しくなっていただくことを意味します。

優先順位

コンポーネントには三種類あります。

下の方が優先されます。どういうことかというと、これらに同じ名前のコンポーネントが登録された時は、展開コンポーネントとして処理されるということです。g:lightline.componentとg:lightline.component_functionの両方に登録された時は、関数コンポーネントとして処理されます。

lightline.vimのデフォルトのコンポーネントは、殆ど普通のコンポーネントで登録されています。つまり、ユーザーが同名のコンポーネントを関数コンポーネントとして登録すると、ユーザーの設定が優先されます。もちろん、ユーザーが普通のコンポーネントとして登録しても、ユーザーの設定が優先されます。

まとめ

展開コンポーネントは、難しいところだと思います。しかし、これがなければlightlineはtablineをサポートできませんでした。重要な概念ですので、理解して欲しいと思います。