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

lightline.vimをカスタマイズする

vim

Vim Advent Calendar 2012 の325日目の記事です。
少し長めになりますので、お時間があるときにお読みください。

Vimのステータスラインを改造するプラグインが、Lokaltog/powerlinebling/vim-airlineに続いてitchyny/lightline.vimが登場しました。
私は今までステータスラインやタブラインは自前で改造していましたが、モダンなラインも経験してみたかったので、導入することにしました。

(当記事の内容)

[lightlineの設定方法]

まずコンポネントを用意します。そのコンポネントの中から表示したいものを選択し、ラインの各配列に並べていけば完成です。
標準のコンポネントg:lightline.componentは以下のようになっています。

let g:lightline.component = {
  \ 'mode': '%{lightline#mode()}',
  \ 'absolutepath': '%F',
  \ 'relativepath': '%f',
  \ 'filename': '%t',
  \ 'modified': '%M',
  \ 'bufnum': '%n',
  \ 'paste': '%{&paste?"PASTE":""}',
  \ 'readonly': '%R',
  \ 'charvalue': '%b',
  \ 'charvaluehex': '%B',
  \ 'fileencoding': '%{strlen(&fenc)?&fenc:&enc}',
  \ 'fileformat': '%{&fileformat}',
  \ 'filetype': '%{strlen(&filetype)?&filetype:"no ft"}',
  \ 'percent': '%3p%%',
  \ 'percentwin': '%P',
  \ 'lineinfo': '%3l:%-2v',
  \ 'line': '%l',
  \ 'column': '%c'
  \ 'close': '%999X X ' }

これらをg:lightline.active.leftにこのように設定すれば

let g:lightline.active.left = [['mode', 'paste'], ['readonly', 'filename', 'modified']]

ステータスラインがアクティブであるときの左側の領域に、一番目のグループとしてmodeとpasteの情報が、二番目のグループとしてreadonlyとfilenameとmodifiedが表示されます。同じグループでは同じハイライトが使われます。
お手軽にやりたいのなら標準のコンポネントの中から表示したいものをセレクトして並べればOKです。
私は少し凝ったことをしたいのでコンポネントをいくつか自作することにします。

ステータスラインについて、設定できる場所は以下のようになっています。

g:lightline.active.left アクティブなステータスラインの左側の領域
g:lightline.active.right アクティブなステータスラインの右側の領域
g:lightline.inactive.left アクティブでないステータスラインの左側の領域
g:lightline.inactive.right アクティブでないステータスラインの右側の領域

[以前使っていたステータスラインと同等以上の機能を実現させたい]

私が長い間使っていたステータスライン(とタブライン)はこちらです。
f:id:leafcage:20131021083416p:plain

  • バッファ番号を表示させたい
  • 編集中のファイル名だけでなくディレクトリも知りたい
  • ファイルの総行数を表示させたい
  • カレントディレクトリをタブライン右上に表示させたい
  • ctrlp.vimやunite.vimなど他の、ステータスラインを書き換える系のプラグインまでいじるつもりはない(そのプラグインが設定するデフォルトのステータスラインをそのまま使いたい)ので、標準のステータスラインからあまりに掛け離れたハイライトを使いたくない
  • 現在のカラースキーム(改造したevening)に調和する色を使いたい

lightline.vimはステータスラインだけではなくタブライン('guioptions'オプションに"e"が含まれていないときに利用されるもの。'tabline')の改造も出来るので、ついでにやってみます。
この要件を満たすためにいろいろいじって、出来上がったのがこちらになります。
f:id:leafcage:20131021083434p:plain

地味目な淡い感じがなかなか良いのではないでしょうか。
以下では実際の設定について解説します。

[lightlineの表示部分を作成する]

(前準備)

以下のコマンドを定義しました。

command! -bar LightlineUpdate    call lightline#init()|
  \ call lightline#colorscheme()|
  \ call lightline#update()

g:lightlineを変更した後でこれを呼ぶと変更内容がすぐに確認できます。

(コンポーネントの作成)

編集中のファイルのディレクトリを知るためのコンポーネントは標準では用意されていません。ので、新しく作る必要があります。
単純な処理なので、文字列の評価だけで何とかなりそうです。
g:lightline.componentを使います。

let g:lightline.component = {}
let g:lightline.component.dir = '%.35(%{expand("%:h:s?\\S$?\\0/?")}%)'

%から始まるのはオプション'statusline'で評価される特別な文字列です。(:h 'statusline')
%.35( %)は括弧内が35文字より長くなったら先頭から切り詰めるという意味です。%{}は、この括弧の中の要素を評価します。

let g:lightline.component.winbufnum = '%n%{repeat(",", winnr())}%<'

%nでバッファ番号を表し、コンマの数でウィンドウ番号を表します。

let g:lightline.component.rows = '%L'

バッファ内の総行数を表します。

let g:lightline.component.cd = '%.35(%{fnamemodify(getcwd(), ":~")}%)'

カレントディレクトリを表します。35文字で切り詰めています。

let g:lightline.component.tabopts = '%{&et?"et":""}%{&ts}:%{&sw}:%{&sts},%{&tw}'

タブ関係のオプション値と'textwidth'を表示します。

let g:lightline.component.lineinfo = '%3l:%-3v'

標準のlineinfoが気に入らないので書き換えました。

少し複雑な情報を表示させるには関数を登録できるg:lightline.component_functionを利用します。
gitの現在ブランチ, current-func-info, そしてVim関数の始まりから数えた現在行数を表示させる関数を作ります。

let g:lightline.component_function = {}
let g:lightline.component_function.fugitive = 'StlFugitive'
function! StlFugitive()
  try
    if &ft !~? 'vimfiler\|gundo' && exists('*fugitive#head')
      return fugitive#head()
    endif
  catch
  endtry
  return ''
endfunction
let g:lightline.component_function.cfi = 'StlCurrentFuncInfo'
function! StlCurrentFuncInfo()
  if exists('*cfi#format')
    return cfi#format('%.43s()', '')
  end
  return ''
endfunction
let g:lightline.component_function.currentfuncrow = 'StlCurrentFuncRow'
function! StlCurrentFuncRow()
  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 ? row : ''
  endif
  return ''
endfunction

タブページ用の関数はg:lightline.tab_component_functionを使います。
この関数にはタブページ番号が引数として渡されます。

let g:lightline.tab_component_function = {}
let g:lightline.tab_component_function.prefix = 'TalPrefix'
function! TalPrefix(n)
  return lightline#tab#tabnum(a:n). TalTabwins(a:n)
endfunction
function! TalTabwins(n)
  return repeat(',', len(tabpagebuflist(a:n)))
endfunction

タブページ番号に、タブページの中にどれだけウィンドウが開かれているのかを,の数で表したものを添加します。

let g:lightline.tab_component_function.filename = 'TalFilename'
function! TalFilename(n)
  return TalBufnum(a:n). '-'. substitute(lightline#tab#filename(a:n), '.\{16}\zs.\{-}\(\.\w\+\)\?$', '~\1', '')
endfunction
function! TalBufnum(n)
  let buflist = tabpagebuflist(a:n)
  let winnr = tabpagewinnr(a:n)
  return buflist[winnr - 1]
endfunction

バッファ番号と、ファイル名(長ければ16文字にまで切り詰め)を表示します。

[ピースを填(は)める]

準備は整いました。後はこれらのコンポーネントをパズルのピースのごとく各場所に填(は)め込んでいくだけです。

let g:lightline.active = {}
let g:lightline.inactive = {}
let g:lightline.active.left = [['winbufnum'], ['dir'], ['filename'],
  \ ['filetype', 'readonly', 'modified'], ['currentfuncrow']]
let g:lightline.active.right = [['lineinfo'], ['percent'],
  \ ['fileformat', 'fileencoding'], ['cfi']]
let g:lightline.inactive.left = [['winbufnum'], ['dir'],
  \ ['filename'], ['filetype', 'readonly', 'modified']]
let g:lightline.inactive.right = [['lineinfo'], ['percent'], ['fileformat', 'fileencoding']]

現在アクティブなウィンドウとそうでないウィンドウのステータスラインです。非アクティブでも情報は欲しいので、表示する内容を極端に削ったりはしていません。
リストの中のリスト1つに付き別の色で表示されます。同じリストの中にある要素は同じ色が使われます。区切りもg:lightline.separatorが使われるか、g:lightline.subseparatorが使われるかといった違いがあります。

let g:lightline.tabline = {'right': [['rows'], ['cd'], ['tabopts'], ['fugitive']]}
let g:lightline.tab = {'active': ['prefix', 'filename']}
let g:lightline.tab.inactive = g:lightline.tab.active

g:lightline.tablineがタブラインです。右側に現在バッファの総行数とカレントディレクトリと補佐的な情報を表示しています。
g:lightline.tabはタブページを示すタブの一つ一つに表示する内容です。非アクティブでも表示内容は変更しないようにしました。

[lightlineカラースキームを設定する]

カラースキームを変更するにはg:lightline.colorschemeにカラースキーム名を設定します。
するとg:lightline#colorscheme#{カラースキーム名}#paletteという変数から設定が読み込まれます。
ということはlightline/colorscheme/{カラースキーム名}.vimというファイルを用意しないといけないのかと思われがちですが、この変数はvimrcで定義することが出来ます。別にファイルを用意する必要はありません。
g:lightline#colorscheme#{カラースキーム名}#paletteは色設定を収めた辞書になっています。長い名前なので、s:pというような変数を用意して設定を書いて、最後に

let g:lightline#colorscheme#lclightline#palette = s:p

という形で代入するようにするといいでしょう。
まずは以下のように書きます。

let g:lightline.colorscheme = 'lclightline'
let s:p = {'inactive': {}, 'normal': {}, 'insert': {}, 'visual': {}, 'tabline': {}}

g:lightline.colorschemeは任意の名前に変更してください。私はとりあえずlclightlineにしました。
それぞれステータスラインの非アクティブ時、ノーマルモード時、インサートモード時、ビジュアルモード時と、タブラインについての設定です。

let s:STL_BASECOLOR = ['black', 'Gray80', 16, 0]
let s:STL_ATTRIBUTECOLOR = ['white', 'SlateGray', 231, 0]
let s:p.inactive.middle = [s:STL_BASECOLOR]
let s:p.inactive.left = [s:STL_BASECOLOR, ['black', 'azure', 16, 0],
  \ ['black', 'MistyRose', 16, 0], s:STL_ATTRIBUTECOLOR, ['black', 'azure', 16, 0]]
let s:p.inactive.right = [['black', 'NavajoWhite1', 16, 0],
  \ ['black', 'MistyRose', 16, 0], s:STL_ATTRIBUTECOLOR, ['black', 'azure', 16, 0]]

リストの中にあるリストの要素はそれぞれ、[{文字色}, {背景色}, {カラーターミナルの文字色}, {カラーターミナルの背景色}, {文字装飾(省略可能)}]を表しています。
カラーターミナルの文字色なんて書いてられるかーい!という方は、lightline#colorscheme#fill()を使うとカラーターミナルの文字色を自動設定してくれるので便利です。これを使う場合のリストの設定は[{文字色}, {背景色}, {文字装飾(省略可能)}]で済みます。その場合、最後にs:plightline#colorscheme#fill()に渡せば[{文字色}, {背景色}, {カラーターミナルの文字色}, {カラーターミナルの背景色}, {文字装飾(省略可能)}]の形式に変換してくれます。
非アクティブなステータスラインの中央部分は本来のステータスラインの色と同じ色である、背景グレーに文字色黒を選択しました。他のプラグインが一時的にステータスラインを標準色のもので書き換えても違和感を少なくすることが出来ます。
inactive.leftの先頭にも同じ色を使って、あくまでベースはこの色であると主張させます。

let g:lightline.inactive.left = [['winbufnum'], ['dir'],
  \ ['filename'], ['filetype', 'readonly', 'modified']]

のように設定しましたが、この場合、['winbufnum']コンポーネント['black', 'Gray80', 16, 0](s:STL_BASECOLOR)の色が使われ、['dir']コンポーネントには['black', 'azure', 16, 0]が、['filetype', 'readonly', 'modified']の3コンポーネントsubseparatorで区切られた上で['white', 'SlateGray', 231, 0](s:STL_ATTRIBUTECOLOR)の色が使われます。

let s:p.normal.middle = s:p.inactive.middle
let s:p.normal.left = map(deepcopy(s:p.inactive.left), 'extend(v:val, ["bold"])')
let s:p.normal.right = map(deepcopy(s:p.inactive.right), 'extend(v:val, ["bold"])')

ノーマルモードでの色は非アクティブ時の色と同じ物を使っています。ただし、要素の最後に'bold'を追加して太字にするようにします。

let s:p.insert.middle = [['black', 'DarkKhaki', 16, 0, 'bold']]
let s:p.insert.left = [s:p.insert.middle[0], ['black', 'LightSkyBlue1', 16, 0, 'bold'],
  \ ['black', 'RosyBrown1', 16, 0, 'bold'], ['white', 'SlateGray', 231, 0, 'bold'],
  \ ['black', 'LightSkyBlue1', 16, 0, 'bold']]
let s:p.insert.right = [s:p.insert.middle[0], ['black', 'RosyBrown1', 16, 0, 'bold'],
  \ ['white', 'SlateGray', 231, 0, 'bold'], ['black', 'LightSkyBlue1', 16, 0, 'bold']]

インサートモードでは濃い色に変更してインサートに入ったことを主張させるようにします。

let s:p.visual.middle = [['black', 'thistle2', 16, 0, 'bold']]
let s:p.visual.left = deepcopy(s:p.insert.left)
let s:p.visual.left[0] = s:p.visual.middle[0]
let s:p.visual.right = deepcopy(s:p.insert.right)
let s:p.visual.right[0] = s:p.visual.middle[0]

ビジュアルモードでは不思議な雰囲気を醸せるようにしました。実はあまり気に入っていないので改善の余地有りです。

let s:p.tabline.middle = [['black', 'gray', 16, 0]]
let s:p.tabline.left = [['black', 'gray60', 16, 0]]
let s:p.tabline.tabsel = [['white', '#002451', 231, 17, 'underline']]
let s:p.tabline.right = [['black', 'Gray80', 16, 0], ['white', '#002451', 231, 17],
  \ ['black', 'DarkGray', 16, 0], s:STL_ATTRIBUTECOLOR]

タブラインは渋い感じにしました。

let g:lightline#colorscheme#lclightline#palette = s:p
unlet s:p

これにて設定完了です。
この設定部分をまとめて読みたい方は私のvimrcをご覧ください(執筆時点で1130行目当たりから)。
色を決めるときには、色見本を表示させるプラグインがあると便利です。

[ctrlp.vimとの競合を何とかする。]

ctrlp.vimが開くControlPバッファは:noautocmdで開かれるため、lightlineがバッファが開かれたことを検知できず、ctrlp.vimのステータスラインを上書きしてしまいます。しかしctrlp.vimではautoloadの代わりに、ControlPバッファが開かれたときと閉じられたときに実行させる関数を指定することが出来るので、これを利用してlightline.vimがステータスラインを上書きするのを阻止しましょう。w:lightline = 0を指定すれば上書きを防げます。

let g:ctrlp_buffer_func = {'enter': 'CtrlPEnter'}
function! CtrlPEnter()
  let w:lightline = 0
endfunction