cmdheight=0がrevertされたので無理矢理Pluginで頑張ってみた

2022/09/17に公開

Vimのcmdheight=0はrevertされてしまいました
でも一度味わってしまったあの感覚を諦めきれなくてプラグインを作ってみました。

https://github.com/utubo/vim-cmdheight0

こんなかんじになります

実現方法

ゴリゴリにウィドウの配置を計算して疑似statuslineをechoで表示しています。

Install

vim9script
⋮
dein# add('utubo/vim-cmdheight0')
⋮
g:cmdheight0 = get(g:, 'cmdheight0', {})
g:cmdheight0.format = '%t %m%r %=%`%3l:%-2c%`%{&ff} %{&fenc} %L'
# require nerd fonts
g:cmdheight0.tail = "\ue0be"
g:cmdheight0.tail_style = "reverse"
g:cmdheight0.sep  = "\ue0bc"
g:cmdheight0.sub  = "\ue0bb"
nnoremap ZZ <ScriptCmd>cmdheight0#ToggleZen()<CR>
# (デバッグ用)以下を指定するとVimEnter時に起動しなくなります。
#g:cmdheight0.at_start = 0

カスタマイズ

表示形式

  • 一部ですがset statuslineと同じように表示内容を設定できます。
    g:cmdline.format = '%t%=%{&fenc} %{&ff} %l:%c'
    
  • モード表記はg:cmdline.modeでカスタマイズできます。
    g:cmdline.mode.n = 'N' # ノーマル
    
  • 当然区切り線も変更できます。
    # NERD FONTSが使えるならこんな感じ
    g:cmdheight0.tail = "\ue0be"
    g:cmdheight0.tail_style = "reverse"
    g:cmdheight0.sep  = "\ue0bc"
    g:cmdheight0.sub  = "\ue0bb"
    

ベースとなるステータスラインと各モード表記の色の2色だけですが、hilightコマンドで設定できます。(ハイライトグループについてはREADME.mdを見てください)
&statusline%#foo#にも対応させたいのですが、斜め区切りのハイライトとechoを1行に収めるための切捨て処理が大変そうで妥協してます…
また、特に設定しなくてもcolorschemeからそれっぽい色を拾ってくるようにしています。
これについてはlightlineのようにプリセットを用意したいところです。(仕組みだけは組み込んであります)

問題点

statuslineをechoで再現するにあたって以下の問題がありました。

  • 右端まで表示できない(改行されてENTER待ちになってしまう)
  • プラグインとの相性でチラつく
  • statuslineのコピーを自前実装するのは手間

ZEN-mode

画面の次の行を表示してstatuslineを無くすZEN-modeも作ってみました。

:call cmdheight0#ToggleZen()で切り替えられます。
このモードはかなり粗削りで、以下の問題が残っています。

  • 再現した次の行にはカーソルを移動できない
  • 再現した次の行は各プラグインに認識されない(easy-motion等)
  • signやconcealが再現できていない
  • 画面一番下の行が改行されていると表示がダブってしまう(バグ🐞)

popupwinでの実装の問題と可能性

vim-cmdheight0は最初からechoで行こうと考えて実際echoで実装したのですが、
途中チラつきと格闘する羽目になりpopupwinなら問題クリアできるのでは?
と試してみました。
が、以下の難点があり諦めました。

  • popupwinを隠すタイミングを判断できず本来のechoを隠ぺいしてしまう
    (echoでの実装なら本来のechoは上書き表示されるので考えることが少ない)
  • 疑似statuslineのハイライトの調整が以外と面倒

逆に上記をクリアすれば以下の利点が見込めます。

  • 多分echoでやるより安定して表示される(チラつかない)
  • ZEN modeでのシンタックスハイライトとタブストップを再現できる
    (&filetype&tabstopを元のウィンドウと合わせてやれば再現できると思います)
  • 画面右端まで表示できる
  • 画面右端を超えそうでも勝手に切り捨てられるので処理が楽
  • 分割ウインドウでもechoのように1行に無理矢理結合表示せずに複数のpopupwinを使えばOK
  • 頑張れば元のcmdlineはpopupwinで隠して新たにcmdlineを独自実装できるかも?

苦労したところ

分割ウインドウ

疑似statusラインは一番下にあるウインドウの情報を表示するのですが、
縦2つに分割されているとそれは2つになります。
縦横縦横縦縦横横…と分割されていると…
対象のウインドウのリストを取得する処理は数分でサクッとつくりましたがエレガントなコードにできなくて悔しい。

autocmdとモードの表示

モードを変更したりウィンドウを作ったり閉じたり移動したりタブを移動したりすると変わるので色々こねくり回していたら、
もうautocmdの設定がごっちゃごちゃです!ヤバイ!

チラつき

gvimだととにかくチラついて大変でした。
redrawredrawstatusで画面全体がチラついてしまいます。
前者は\rに変更、後者は実行するタイミングを出来るだけ少なくしました。
それでもチラついてしまい最終的には
SafeStateイベントでも再描画するようにしたら解決しました。

:echo

チラつかなくできたと思ったら今度は:echo fooと打っても即上書き表示するようになってしまいました。
これは、以前cmdheight=0を使って自動でcmdlineを隠したり表示したりするようにした経験を生かして&updatetimeの時間だけ再描画を待つようにしました。

%{expr}の展開

基本vim9scriptでつくりましたが、%{expr}の展開はlegacyで行っています。
例えばstatuslineなら、

vim9script
g:foo = 'bar'
def Foo(): stirng
  return 'bar'
enddef
set statusline=%{Bar()}

のようにかけますが、vim9script内でexecute()bar展開すると、 スクリプトローカルなBar()を探してしまいエラーになってしまいます。 ここは%{g:Bar()}と書かなければなりません。 冗長ですしstatuslineと違うと混乱もするのでlegacyで展開するようにしました。 それでもg:buzz = 0%{g:buzz}`ってかかなきゃだめみたい。なんで?

silent!できない!

CursorMovedModeChangedは頻繁に呼びだされるので、エラーになるとほぼ操作不能になってしまいます。
なので

autocmd CursorMoved * silent! Hoge()

のように予防したかったのですがechoも出なくなってしまいました。
結局def Silent(F: func)を作ってtry-catchしてどうにかしました。

def Silent(F: func)
  try
    F() # この中で疑似statuslineをecho
  catch
    # エラー処理
  endtry
enddef
autocmd CursorMoved * Silent(Hoge)

ZEN-mode

「次の行を表示するだけ」と思ってたらSignColumnやLineNrの存在を忘れていました。
特にSignColumnは現在表示されているかを直接判断できませんので、
&textoff&number&numberwidthから逆算するようにしました。

やっぱりlightline.vimはすごい

今回疑似statuslineを作ってみて分かったことはやっぱりligihtline.vimは素晴らしいプラグインということです。
サクッと設定して簡単におしゃれでカッコイイstatuslineを作れる手軽さ素晴らしいです。
設定方法とかカラースキームのプリセットとかとてもよく考えられているなぁと思いました。

終わりに

冒頭にあるように、このプラグインは自分用に作ったもののお裾分けです。
ですが、cmdheight=0再現の可能性は示せたと思います。

※どこまで雑なコードを上げていいのか問題はちょっと気になり始めています。

To be Continued ...

https://zenn.dev/utubo/articles/4cda7d76f486d9

GitHubで編集を提案

Discussion