Vimのステータスラインを自作する(statusline の書き方と既定値)
ステータスラインに何を出すかはstatuslineオプションで決まります。Vimの既定値は空で、そこへset statusline=%f\ %mのように書けば好きな並びにできます。この「空のところに書き足す」という感覚のまま、Neovimの設定にも同じ行を持ち込んでいる人は多いはずです。
ところがNeovim 0.12.6を--cleanで起動して:set statusline?を打つと、空ではなく1行に収まらない長い式が返ってきます。中には診断の件数や進捗の表示が入っていて、自作の値で上書きした瞬間にそれらは黙って消えます。この記事では書式の基本を実機で確かめたうえで、Neovimの既定値を1項目ずつ読み、自作しても必要な部品を残す書き方までをまとめます。
検証環境:Vim 9.1.1752とNVIM v0.12.6(macOS)。どちらも一時ディレクトリで--clean -nを付けて起動し、tmuxの60〜100桁の画面でステータスラインをcapture-paneで取り出しました。掲載している画面の行はその実物です。診断はvim.diagnostic.set()で、進捗はnvim_echo()のkind='progress'で手動で作りました。実際のLSPサーバーにはつないでいません。
目次(9項目)
statusline と laststatus の役割の違い
laststatusは「いつステータスラインを出すか」、statuslineは「出したときに何を書くか」を決めます。表示されないことに悩んでいるなら前者で、条件の選び方はlaststatusでステータスラインを表示/非表示にする設定にまとめてあります。この記事は後者の中身の話です。
既定値は両者で違います。Vim 9.1を--cleanで起動するとlaststatus=1で、ウィンドウを分割したときにしかステータスラインが出ません。Neovim 0.12.6はlaststatus=2で、ウィンドウが1つでも出ます。statuslineはVimが空、Neovimが長い式です。
Vimのstatuslineが空のとき、ステータスラインが何も書かれない帯になるわけではありません。:vsplitした画面ではこう出ました。
b.txt 1,1 All a.lua 1,1 All
ファイル名と、rulerがオンなら行・列・位置が出ます。--cleanのVimはdefaults.vimでrulerがオンになっているので、この形になります。Neovimの既定値でも普通のファイルなら見た目はほぼ同じで、違いが出るのは診断や端末バッファのような「何かが起きたとき」だけです。
最小の自作例と、よく使う % 項目
ファイル名・変更フラグ・ファイルタイプを左に、行と列と位置を右に置く最小の例です。
set statusline=%f\ %m%r%y%=%l/%L\ %c\ %P
Neovimでa.luaを開いた状態では、90桁の画面でこう表示されました。
a.lua [lua] 1/2 1 All
%mは未変更なら何も出さないので、%fの後ろのスペースだけが残り、[lua]が%yです。よく使う項目は次のとおりです。
| 項目 | 出るもの | 実際の表示 |
|---|---|---|
%f | 開いたときのパス | sub/dir/note.txt |
%t | ファイル名だけ | a.lua |
%m | 変更フラグ | [+](未変更なら空) |
%r | 読み取り専用 | [RO] |
%y | ファイルタイプ | [lua] [text] |
%l / %L | 今の行 / 全行数 | 1/2 |
%c | 列(バイト数) | 28 |
%v | 列(画面上の桁) | 35 |
%P | 画面がファイルのどこか | All Top 44% |
%cと%vの28と35は、行頭にタブがある行の末尾で取った値です。タブは1バイトでも画面上は8桁ぶん進むので、2つの値がずれます。既定のrulerが2,28-35のように両方を出すのはこのためです。
書くときにつまずきやすいのはスペースです。:setはスペースを値の終わりとみなすので、Vimで:set statusline=%f %mと打つとE518: Unknown option: %mになります。値の中のスペースは\ と書きます。set statusline+=\ %yで少しずつ足していくときも同じです。エスケープが煩わしければlet &statusline = "%f %m %y"と書けば、文字列なのでスペースをそのまま書けます。
右寄せの %= と幅の指定(%-14. や %<)
%=は、そこから後ろを右端に寄せる区切りです。上の例では%=より前がファイル名側、後ろが行と列の側になっています。
項目には%-最小幅.最大幅の形で幅を付けられます。[%8.l]は行番号を8桁に右詰めして[ 1]と出し、-を付けると左詰めになります。複数の項目をまとめて幅を決めるには%(と%)で囲みます。違いが見えるのはカーソルを動かしたときです。
" 幅なし:列の桁数が変わるたびに All の位置が動く
set statusline=%f%=%l,%c%V\ %P
" 2,28-35 All と 1,1 All で All がずれる
" 幅14の左詰め:All の位置が固定される
set statusline=%f%=%-14.(%l,%c%V%)%P
" 2,28-35 All
%<は、画面に収まらないときにどこから削るかの指定です。指定しなければ先頭から削られます。[%t]\ %<%{repeat('x',80)}%=ENDと%<なしの版を60桁の画面で比べると、こうなりました。
[a.lua] <xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxEND %< あり
[<xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxEND %< なし
削られた位置には<が出ます。ファイル名は残したいので、長くなりうる項目の手前に%<を置くのが定石です。
式を埋め込む %{} と %{%%} と %!
オプションの値だけでは足りないとき、Vim scriptの式を埋め込めます。形が3つあり、違いは「式の結果に%が含まれていたらどう扱うか」です。結果に%lを含む式で確かめました。
| 書き方 | 値 | 表示 |
|---|---|---|
%{} | A%{'%l'}B | A%lB |
%{%%} | A%{%'%l'%}B | A1B |
%! | %!'X%l' | X1 |
%{}は結果を文字としてそのまま出し、%{%%}は結果をもう一度書式として読み直します。%!は値全体を式とみなし、その結果を書式にします。%{%%}はVim 9.1でも同じくA1Bになりました。後で見るとおり、Neovimの既定値はほぼすべての部品を%{%%}で書いていて、ここが自作するときの要になります。
%{}の式は、そのステータスラインを描いているウィンドウを一時的にカレントにして評価されます。win_getid()はどのウィンドウでも「自分」を返すので、本当のカレントと比べるにはg:actual_curwinを使います。:help stl-%{によると、これは本当のカレントウィンドウのIDを文字列で持つ変数です。%!の式で参照するとE121: Undefined variable: g:actual_curwinになったので、使えるのは%{}の中だけです。
なお式はステータスラインを描き直すたびに評価されます。重い処理を直接書くと、カーソルを動かすだけで引っかかるようになります。
色を付ける %#Group# と、カレントのウィンドウだけに出す
%#グループ名#から後ろはそのハイライトグループの色になり、%*で元の色に戻ります。set statusline=%#ErrorMsg#%f%*\ %mをVimで分割した両方のウィンドウに当てて色を見ると、ファイル名はカレントでも非カレントでも同じ赤地のままでした。%*の後ろだけが、カレントではStatusLine、非カレントではStatusLineNCの色に変わります。%#Group#で指定した色は、非カレント用に暗くなったりしません。
色ではなく表示そのものをカレントだけに出すなら、前の節のg:actual_curwinを比べます。
set statusline=%f%{win_getid()==g:actual_curwin?'\ [cur]':''}
b.txt [cur] b.txt
左がカレントです。ウィンドウごとに中身を変えたいときはsetlocal statusline=で、そのウィンドウだけの値を持たせられます。ただし手元で試したところ、ローカルの値を持ったウィンドウで:set statusline+=%=%l/%Lを打つと、ローカルの値はグローバルに足した値で置き換わり、setlocalで付けた中身は消えました。ローカルの値を残したいウィンドウでは:setではなく:setglobalで足します。
Neovim 0.12 の既定の statusline を読む
NVIM v0.12.6を--cleanで起動したときの値です。見やすいように部品ごとに改行していますが、実際は1行です。
%<%f %h%w%m%r %{% v:lua.require('vim._core.util').term_exitcode() %}
%=
%{% luaeval('(package.loaded[''vim.ui''] and vim.api.nvim_get_current_win() == tonumber(vim.g.actual_curwin or -1) and vim.ui.progress_status()) or '''' ')%}
%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}
%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}
%{% &busy > 0 ? '◐ ' : '' %}
%{% luaeval('(package.loaded[''vim.diagnostic''] and next(vim.diagnostic.count()) and vim.diagnostic.status() .. '' '') or '''' ') %}
%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}
それぞれの部品を実際に表示させて確かめました。
| 部品 | 出る条件 | 実際の表示 |
|---|---|---|
| 終了コード | 端末バッファのプロセスが終わった | [Exit: 3] |
| 進捗 | 進捗メッセージが走っている、かつカレント | 70%(1) |
| 入力中のキー | showcmdloc=statusline | 2を押すと2 |
| キーマップ | keymapを設定した | <ru> |
| busy | busyが1以上 | ◐ |
| 診断 | バッファに診断がある | E:1 W:2 |
| ルーラー | rulerがオン | 1,1 All |
終了コードは:terminal sh -c 'exit 3'で、キーマップは:set keymap=russian-jcukenwinで確かめました。busyはNeovim 0.12で入ったバッファローカルのオプションで、ヘルプには意味はバッファを持つプラグインが決めると書かれています。手で:set busy=1にすると、ルーラーの手前に◐が出ました。
読んでいて目に付くのは、ほとんどの部品が%{%%}で書かれていることです。理由は部品の返す値にあります。vim.diagnostic.status()を:lua print()で見ると%#DiagnosticSignError#E:1 %#DiagnosticSignWarn#W:2%##で、色の指定が埋め込まれています。vim.ui.progress_status()は70%%(1)の形を返します。どちらも書式として読み直さないと正しく表示されません。試しに%{v:lua.vim.ui.progress_status()}と%{}で書くと、画面には70%%(1)と%が2つ出ました。
進捗と診断の部品はpackage.loadedを見て、モジュールが読み込まれていなければ何もしません。ステータスラインを描くためだけにvim.uiやvim.diagnosticを読み込ませない作りです。進捗の部品にはg:actual_curwinの比較も入っていて、分割していてもカレントのウィンドウにしか出ません。
自作しても診断と LSP の進捗を残す書き方
診断を3件(エラー1、警告2)置いた状態で、Neovimの既定値と最小の自作例を比べました。
a.lua E:1 W:2 1,1 All 既定値
a.lua [lua] 1/2 1 All 自作
自作の値に変えた時点でE:1 W:2が消えます。エラーも警告も出ていないように見えるので、気づかないまま過ごしがちです。部品を%{%%}で足し戻すと、色付きで戻りました。診断の出し方そのものを変えたいときはvim.diagnostic.configの設定のほうで扱います。
set statusline+=\ %{%v:lua.vim.diagnostic.status()%}
a.lua [lua] 1/2 1 All E:1 W:2
進捗も戻すなら、init.luaに関数を1つ置いて%{%%}から呼びます。既定値と同じく、カレントのウィンドウだけに出すための比較を入れてあります。
function _G.my_progress()
if vim.api.nvim_get_current_win() ~= tonumber(vim.g.actual_curwin) then
return ''
end
return vim.ui.progress_status()
end
vim.o.statusline = '%f %m%r%=%{%v:lua.my_progress()%}%{%v:lua.vim.diagnostic.status()%} %l/%L'
この設定で分割し、進捗メッセージを1つ流すとa.lua 40%(1) 1/1 a.lua 1/1となり、左のカレントだけに出ました。
注意点が2つあります。1つ目は、vim.ui.progress_status()が進捗を数え始めるのは最初に呼ばれた時点からだということです。vim/ui.luaを読むと、Progressイベントを受け取る自動コマンドは初回の呼び出しで作られます。実際、一度も呼んでいない状態で進捗メッセージを2つ流してから呼ぶと空文字が返り、次の更新から70%(1)と数えられました。ステータスラインに入れておけば起動直後に描画で呼ばれるので、普段は問題になりません。
2つ目は、0.12.6の時点ではLSPの進捗がそのままではprogress_status()に入らないことです。ランタイムをgrepすると、kind = 'progress'で進捗メッセージを出しているのはvim.packと:checkhealthだけで、LSPの$/progressはLspProgressという別の自動コマンドを起こすだけでした。つまり既定値の進捗の部品が拾うのも、プラグインの導入や更新の進み具合です。vim.packについてはNeovim 0.12の新機能で扱っています。:help LspProgressには、LSPの進捗を進捗メッセージに変換する例が載っています。
vim.api.nvim_create_autocmd('LspProgress', {
callback = function(ev)
local value = ev.data.params.value
vim.api.nvim_echo({ { value.message or 'done' } }, false, {
id = 'lsp.' .. ev.data.params.token,
kind = 'progress',
source = 'vim.lsp',
title = value.title,
status = value.kind ~= 'end' and 'running' or 'success',
percent = value.percentage,
})
end,
})
ヘルプの例はbuffer = buf付きですが、ここではどのバッファでも拾うように外しています。nvim_exec_autocmds()でLspProgressを手で起こしたところ、beginで30%(1)が出て、endで消えました。本物のLSPサーバーの進捗では試していません。
進捗の部品を自作の値に移したとき、最初は%{}で書いてしまい、画面に70%%(1)と出ました。関数の戻り値を何度見直しても%は2つで、関数が壊れていると思い込んでしばらくui.luaを読んでいました。もう一つ手間取ったのが、:lua print(vim.ui.progress_status())が何度やっても空文字を返したことです。進捗メッセージを流したあとに初めて呼んでいたので、数え始める前のメッセージだったわけです。どちらも既定値の書き方を最初に読んでいれば避けられました。
まとめ
statuslineは%項目を並べ、スペースを\ で書き、%=で左右に振り分けるのが基本です。式を埋め込むときは、結果を文字のまま出す%{}と、もう一度書式として読む%{%%}を使い分けます。色やカレントだけの表示は%#Group#とg:actual_curwinで作れます。
Neovim 0.12で自作するなら、既定値が空ではないことを先に知っておくと安全です。上書きすると診断の件数と進捗が消えるので、vim.diagnostic.status()とvim.ui.progress_status()を%{%%}で呼んで戻します。LSPの進捗まで出したいときは、LspProgressから進捗メッセージを作る1段が要ります。タブの行も同じ書式で書けるので、表示の条件はshowtablineでタブバーを表示/非表示にする設定を合わせて見てください。