Neovim のフロートウィンドウ機能を使って git-messenger.vim をつくりなおした

git-messenger.vim は,カーソル下の行のコミット情報を表示する Vim プラグインです.

github.com

他所のプロジェクトのコードや OSS のコードを読んでいると,なぜこうなってるんだろう?と思うことがよくあります.コミットメッセージをまともに書いているプロジェクトでは,その部分の変更を加えたコミットメッセージに答えがあることがあるのですが,毎回 git blame で対象のコミットを引っ張ってきて git show で読むのは結構大変です.

git-messenger.vim では,その負担を軽減するために,(1) カーソルがいる行にコミット情報を git から引っ張ってきて (2) 良い感じに表示する という機能を提供します.

もう6年も前につくったプラグインなのですが,Vim の制約上いくつかの問題があり,syohex さんの Emacs port に比べて良いものになりませんでした.Vim のバルーン(マウスを一定時間バッファ上に置いておくと出せるツールチップ)が GUI にしか対応していないのが主な理由でした.

先日,Neovim にフロートウィンドウ機能が入り,CUI でもツールチップのような UI が簡単に使えるようになったので,git-messenger.vimフルスクラッチでつくりなおしました.Neovim のフロートウィンドウについては後の章でまとめました.

git-messenger.vim

インストール

すべて Vim script で実装されているので,他のプラグインと同様に rhysd/git-messenger.vim リポジトリをインストールするだけで OK です.あともちろん git コマンドも要ります.

コマンドの実行は以前は system() を使っていたのですが,大きいリポジトリでもユーザの入力をブロックしないように job を使って書いています.なので,Neovim または Vim (8 以降)が必要です.

また,フロートウィンドウは最近 Neovim の master に入ったところなのでまだ安定版としてはリリースされていません.開発版の 0.4.0-dev を使う必要があります.例えば macOS であれば brew install neovim --HEAD で入ります.

使い方

:GitMessenger もしくはデフォルトでマップされている <Leader>gm を入力すると,カーソルしたのコミット情報がポップアップウィンドウで表示されます.

  • Commit hash
  • Author, Committer
  • Summary (コミットメッセージの1行目)
  • Body (コミットメッセージの2行目以降)

ポップアップウィンドウは Neovim 0.4.0 以降では前述のフロートウィンドウで,それ以外ではプレビューウィンドウで実装されています.その後カーソルを動かすとポップアップは自動で閉じます.

https://github.com/rhysd/ss/blob/master/git-messenger.vim/demo.gif?raw=true

基本的にはこれだけですが,ポップアップを開いたあとにカーソルを動かさずそのままもう一度 :GitMessenger コマンドか <Leader>gm を入力するとポップアップウィンドウの中にカーソルを移動できます.コミットメッセージが長すぎて全部表示できなかった時のスクロールやメッセージのクリップボードへのコピーなどができます.

ウィンドウ内ではいくつかローカルなマッピングが定義されており,o でより古いコミットを手繰ることができます.直近のコミットにほしい情報が書いてあるとは限らない(例えばフォーマッタでの整形が挟まったり,別のリファクタリングが挟まったりなど)ので,そういうときはより古いコミットのメッセージにほしい情報があることがあります.さらに O で新しいコミットに戻ったり,q でウィンドウを閉じたりできます(? でヘルプ)

https://github.com/rhysd/ss/blob/master/git-messenger.vim/history.gif?raw=true

カスタマイズ

  • デフォルトのマッピング <Leader>gm が気に入らない場合は g:git_messenger_no_default_mappingsv:true をセットして <Plug>(git-messenger) をマップすれば OK です
  • 他にもいくつか <Plug> マップが定義されています
  • いくつかの挙動はグローバル変数で制御できるようになっています
  • Neovim のみポップアップウィンドウ内のハイライトをカスタマイズできます.デフォルトの色合いがお使いのカラースキームに合わないときは自分で色をカスタマイズできます.Neovim のみなのはウィンドウローカルにハイライト色を変更できる winhighlight というオプションを Neovim だけが持っているためです

詳しくはリポジトリREADME.md:help git-messenger で確認できます.

Neovim のフロートウィンドウについて

Vim のウィンドウは分割することでタイル型に配置されるレイアウトしか対応しておらず,ウィンドウが重なるようなレイアウトは(無理矢理バッファを書き換えてそれっぽく見せるようなことをしない限り)実現できませんでした.

そこで Neovim では CSSposition: relativeposition: absolute のように位置基準でウィンドウの重なりを許すウィンドウレイアウトを実装しました.

何が良いのか

この機能の設計で特に優れていると感じる点は,レイアウト以外はほぼ完全に普通の Vim のウィンドウと同じところです.

  • CUI で使える
  • プラグイン開発者はウィンドウの開き方だけ分かれば,あとは普通の Vim のウィンドウと同様に扱える
  • ウィンドウ内のコンテンツは普通の Vim のバッファなので,setline() などで自由に追加・変更・削除できる
  • filetype をセットしたり,自由にハイライトできる
  • ウィンドウを開く以外はバルーンのような専用の API が要らない

この機能により,プラグイン開発者はウィンドウの上にオーバーレイするような UI を変なハックをしたり妥協することなく実装することができます. ぱっと思いつくのは

  • 補完ウィンドウの自作
  • ドキュメント情報表示ツールチップ
  • fuzzy finder の選択ウィンドウ(VS Code のような)
  • ターミナル表示

などに使えそうです.

使い方

フロートウィンドウを開くのには nvim_open_win(),サイズや配置を変えるのには nvim_win_set_config() を使います.

" 開くウィンドウの幅
let width = 40

" 開くウィンドウの高さ
let height = 10

" ウィンドウを開いたあと,カーソルをそのウィンドウ内に移動するか
let enter = v:true

" カーソルを :wincmd やマウスでウィンドウ内に移動できるか
let focusable = v:true

" 何に対して相対的にウィンドウの配置位置を決めるか
"   - "editor": エディタのスクリーンに対して.スクリーン上の絶対座標で指定
"   - "win": 現在のウィンドウ位置に対して.これを使う場合はオプションに 'win' というキーで別途対象ウィンドウのウィンドウ ID を指定する
"   - "cursor": カーソル位置に対して
let relative = 'cursor'

" 基準となるウィンドウの角を四隅のどこにするかを指定します(デフォルト "NW"
"   - "NW": 左上
"   - "NE": 右上
"   - "SE": 右下
"   - "SW": 左下
let anchor = 'NW'

" 'relative' で指定した位置に対する相対的なオフセット
let row = 1
let col = 0

" Neovim 内ではなく,GUI フロントエンド側にウィンドウを出すよう依頼するか
let external = v:false

" 現在のバッファをフロートウィンドウで開く
" カーソルのすぐ下に 40x10 のウィンドウが開かれる
let win_id = nvim_open_win(bufnr('%'), enter, {
    \   'width': width,
    \   'height': height,
    \   'relative': relative,
    \   'anchor': anchor,
    \   'row': row,
    \   'col': col,
    \   'external': external,
    \})

" 新しいバッファを開いたり
enew

" 普通に setline() を使ってコンテンツをセットしたり
call setline('.', ['hello', 'world!'])

" filetype を指定してハイライトを定義したり
set filetype=ruby bufhidden=wipe nomodified buftype=nofile

" フロートウィンドウを別のハイライトで描画したり
set winhighlight=Normal:MyNormal,NormalNC:MyNormalNC

" nvim_win_set_config() でウィンドウのレイアウトをやり直せる
call nvim_win_set_config(win_id, {
    \   'width': 60,
    \   'height': 30,
    \   'relative': 'editor',
    \   'row': 10,
    \   'col': 10,
    \})

フロートウィンドウにはボーダーが無いので,デフォルトのままではフロートウィンドウと下のウィンドウの境界がわかりません.なので特に最後の既存のウィンドウと別のハイライトで背景色を描画するのは重要です.また,フロートウィンドウ内で新しいバッファを開いた際はウィンドウを閉じると中身も自動で開放されるよう bufhidden=wipe を指定しておきます.

nvim_win_set_config() でウィンドウのレイアウトをやりなおすことができるので,これを使ってウィンドウのサイズ変更や位置変更をします.

ちなみに rowcolwidthheight がエディタのスクリーンからはみ出してしまってもエラーになりません.Neovim はなるべくウィンドウをスクリーン内に収めるように描画します.git-messenger.vim では現在のカーソル位置からポップアップを出すのに十分な幅・高さがあるかを確認し,カーソルの上下左右にフロートウィンドウを出し分けます.