目黒.vim #16 に参加して Vim script に const を実装するパッチを書いた

あいにくの雨でしたが,Meguro.vim #16 に参加しました.

目黒.vimVim に関したり関しなかったりする作業をするもくもく会で,今回は Vim:const を追加するパッチを書きました.

github.com

:const コマンドの機能

Vim script では変数を :let で定義・代入します.ですが,

  • 新しい変数定義時も代入時も同じ :let を使う
  • if などで新しいスコープが作成されない(動的スコープ)

により,意図せず既存の変数を上書きしてしまうケースがあります.

" Define variable
let i = 0

if some_condition
    " In heavily nested or big statements...
    let i = 1 " Unexpectedly using the same name variable
endif

echo i
" => 1 (expected 0)

意図せず変数が上書きされないようにするためのコマンドとして :lockvar があります.他のプラグインなどから勝手に上書きされないようにグローバル変数をロックするのに使われたりはしますが,ローカル変数に使うほどのお手軽さはありませんでした.

  • 毎回ロックするのは面倒だし忘れる
  • 余分に1コマンド実行するコスト(パースおよび解釈)がかかる

一方,JavaScript では ES2015 から const が追加されました.これは JavaScript の変数定義をレキシカルスコープにし,参照を変更不能にする構文です.

const number = 42;
number = 99;  // TypeError: invalid assignment to const `number'

Vim script でも内部的に :lockvar の実装を使えば,これと似たような実装ができると思い, 変更しない変数の定義に :let の代替として使える :const を実装しました.:let の代わりに :const を使うだけなので,別途 :lockvar を使うのに比べ面倒ではありません.また,変数の定義と同時にロックするので別コマンドで実行するのに比べてコストが遥かに安いです(実際,ただフラグを立てるだけ).

" Define locked variable
const i = 0

if some_condition
    let i = 1 " Error! `i` is locked
endif

echo i
" => 0 (expected)

リストの分解など複雑な定義式も :let と同様に使えます.

const [a, b, c] = [1, 2, 3]
const [x; xs] = [1, 2, 3]

さらに,:const は安全のため,既存の変数を上書きできないようになっています.もし既存の変数をロックしたい場合は従来どおり :lockvar を使ってください.

let i = 1
const i = 2 " E995 cannot modify existing variable

:const でロックする深さは 1 です(:lockvar 1 相当).なので例えばリストを定義した時,要素まではロックされません.

const l = [1]
call add(l, 2)
echo l
" => [1, 2]

なぜ再帰的に全ての値をロックする :lockvar! 相当にしないのかというと,

let a = 1
let l = [a]
lockvar! l " 再帰的にロックするので a もロックしてしまう
let a = 2 " エラー!a はロックされているので変更できない

のように意図せず変数をロックしてしまう可能性があるからです.また,ネストが深い辞書やリストをロックすると深さに比例したコストがかかってしまいます.

その他,注意する点は

  • その性質上,環境変数$FOO)やオプション値(&filetype)には :const は使えません(E996
  • :unlockvar で無理やり :const で定義した変数のロックを解除することが可能です
  • JavaScriptconst はスコープがレキシカルですが,Vim script の :const はスコープがダイナミックのままです
  • JavaScript 処理系はコードのパース時に const 定義された変数への再代入を検知してエラーにできますが,Vim スクリプト処理系は素朴なインタープリタなので :const で定義された変数が実際に書き換えられようとするまでエラーになりません

:const コマンドの実装

diff はこちら. src/eval.cex_const() を定義し, src/ex_cmds.h に追加したいコマンドを定義して ex_const 関数ポインタを登録しておくと,:constex_const() が呼ばれます.

ちなみに新しいコマンドを追加した時は make cmdidxs でコマンド検索表である src/ex_cmdidxs.h を再生成する必要があります.

:const は変数をロックする以外は :let と同じ挙動のため,実装を :let と共有しています.定義した変数をロックするかどうかのフラグを :let の実装関数に追加し,フラグが立っている場合は変数定義時に di_flagsDI_FLAGS_LOCK フラグと di_tv.v_lockVAR_LOCKED フラグを立てるだけです.複合代入(+= など)や環境変数への代入やオプション変数への代入のパスはフラグが立っているとエラーにします.

テストの作成方法は src/testdir/README.txt に書いてありますが,src/testdir 内に test_const.vim を作成し,src/testdir/Make_all.mak をちょっといじるだけでかなり簡単に実装できます.test_const.vim 内に Test_* な名前の関数を定義しておくと, make test_const でテストが実行できます.

:const コマンドのデバッグ

変数に値をセットする関数 set_var_lval() で,一時的に読み先の文字に NUL をセットしてあとで復帰している部分にエラーチェックを追加した際に復帰処理を追加するのを忘れてエンバグしてしまいました(const [a] = ... の解釈途中で Internal error).

Vim では標準出力はエディタが使っているので printf() などで出力することは基本的にできません.デバッグ方法は src/README.md に書いてあり,

  • デバッグプリントを ch_log() で出しておき,デバッグのための Vim 起動時に :call ch_logfile('log.txt', 'w') のようにしてファイルにデバッグログを吐く.タイミング問題を追う時などに便利
  • :packadd termdebug し,:TermdebugVim の中で Vim を起動して gdb を使ってデバッグする.Macgdb を使うには実行ファイルをコード署名する必要があり若干面倒ですが,大変便利です

vim/vim へのプルリク

ドキュメントを書いて,パッチを GitHubvim/vim リポジトリにプルリクします.

github.com

新機能の実装の場合,取り込まれるかどうかは作者の Bram さん次第なのでどうかなと思ったのですが,サクッとマージしてもらえました(Vim では変更をパッチとして取り込むので,GitHub のマージ機能を使っていません).Bram さんは普段 JavaScript や TypeScript を書くらしく,同じことを考えたことがあったらしいです.Vim script の新機能を Bram さんに説明したいときは JavaScript を引き合いに出すと良いのかもしれません.

あまりに速くマージされたので,いくつか細かいミスに後から気付き追加のパッチを送りました.

github.com

Vim on Wasm on Web Worker on Browser with Atomics

この記事は以前の

rhysd.hatenablog.com

の続編で,WebAssembly (Wasm) にポーティングした Vim の話です.

github.com

TLDR

Wasm にコンパイルした Vim のコードを Web Worker(ワーカスレッド)の中で動かすことで,メインスレッドで行われるユーザのインタラクションをエディタがブロックしなくなりました. また,イベントループのポーリングを Atomics.wait() でやってキー入力を共有メモリバッファで受け取ることで Emterpreter を捨て,実行速度・安定性・バイナリサイズ・ビルド時間・メンテ性が向上しました.

実装:

Run Vim in Web Worker and say goodbye to Emterpreter by rhysd · Pull Request #30 · rhysd/vim.wasm · GitHub

これまでの問題点

前回の記事では Vim のコードベースに手を入れて emscripten と binaryen で Wasm にビルドし, <canvas/> に描画する JavaScript ランタイムを整備することでブラウザ上で動く Vim を実現しました.

ポーティングは概ね問題はありませんでしたが,一点 sleep() が Wasm 及び JavaScript では実現できないという問題がありました.

ウェブページでメインスレッドをブロックすることはユーザとのインタラクションをブロックすることを意味します.なので JavaScript では時間がかかる処理はすべて非同期な API として実装され,ブロックできないようになっています.これと同様にメインスレッドで実行される Wasm もスレッドをブロックする機能は提供されていません.

ですが,C プログラムは sleep() を使っているものがあり,Wasm 移植ではこれが問題になることがあります.Vim では GUI フロントエンドが実装する必要がある関数 gui_mch_wait_for_chars()ブロッキングでユーザの入力を待つ処理を要求するため,これに該当しました.

そこで前回の実装では emscripten が提供する Emterpreter という機能を利用して解決しました.Emterpreter は sleep() ができない代わりに emscripten_sleep() などのマジック関数を提供します.これらは C のコードからは sleep() のように同期的に待つ関数のように使えますが,実際は一旦処理を中断して非同期に待つ処理に変換されます.emccemscripten の C コンパイラ)は emscripten_sleep() を使っている関数を直接 Wasm にコンパイルせず,Wasm の上で動くインタープリタ (Emterpreter) で実行するためのバイトコードに変換します (emterpretify).実行時にはそのバイトコードインタープリタで実行し,emscripten_sleep() の呼び出し箇所で一旦実行状態を保存(suspend)して JavaScript 側の setTimeout() を読んで実行を中断します.タイマーがコールバックを呼んだら Wasm 側に戻り,インタープリタはその中で保存しておいた実行状態を復帰(resume)して実行を再開します.

このような力技により前回はどうにか Vim を動かすことに成功しました.ですが,前回の記事にも書いたように,次の問題が残りました

  • Emterpreter で実行される関数は Wasm から直接呼び出すことができません(逆に Emterpreter から Wasm の関数は呼べる).なので,emscripten_sleep() を呼んでいる関数を呼んでいる関数,それを呼んでいる関数,... というふうに呼び出し元の関数も Wasm には直接コンパイルできず,Emterpreter のバイトコードコンパイルする必要があります.Vimsleep() が必要な関数(gui_mch_wait_for_chars())は Vim のメインループの一番奥にあたる箇所で呼ばれる関数だったため,それを呼ぶコールスタック上の大量の関数を emterpretify する必要がありました.emterpretify する関数は手動でリスト化して管理する必要があるため,そのリストが膨大になってメンテが大変になりました.しかもこのリストが間違っていて Wasm にコンパイルした関数から emterpretify された関数を呼ぼうとすると実行時にプログラムがクラッシュします
  • Emterpreter バイトコードへのコンパイルは,同期的な C のコードを非同期なコード( emscripten_sleep() の呼び出し箇所で処理を切ってコールバックを待つ)に置き換えるため,かなり大きな変換が走ります.そのためコンパイル時間がかなり延びます
  • Emterpreter バイトコードはかなりシンプルなので,その分バイトコードのバイナリサイズは膨れます
  • Emterpreter は試験的な機能なので,かなりバグが多く不安定です.特に JavaScript 側から emterpretify された C の関数を呼ぶ時は不安定で, char * を渡そうとすると何故かクラッシュしたりします(ccall(){async: true} がバグっている説)

上記の理由からどうにか Emterpreter を使うのをやめるのが再優先事項でした.

  • Sync XHR を使って同期でリクエストを投げ,ServiceWorker で受け取って待ってからレスポンスを返すことでなんとか sleep() を実現しようとする → Chrome ではバグで動かず,Firefox ではビジーループに近いほど CPU 利用率が上がるためダメ
  • Vim のメインループ内で gui_mch_wait_for_chars() を呼ぶ関数をすべて非同期(結果を return でなくコールバックで受け取る)に書き換え,emscripten非同期メインループ機能でメインループを回す → 変換が大変すぎる.10000行ぐらい書き換えてみたところで先が見えないのでメンテが厳しすぎると判断し断念

など試してみましたが,うまくいきませんでした.

今回の実装の改善

Atomics API

emscripten の pthread 対応のコードを呼んでいた時に,JavaScript同期プリミティブを提供する Atomics API があることを知りました. その中に Atomics.wait() という futex システムコールのような機能を提供する関数があるのとを見つけました.

The static Atomics.wait() method verifies that a given position in an Int32Array still contains a given value and if so sleeps, awaiting a wakeup or a timeout. It returns a string which is either "ok", "not-equal", or "timed-out".

_人人人人人人人人_
> if so sleeps <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^ ̄

なんと,無いと思っていた JavaScriptsleep() がこんなところにありました.

ですが,これを呼ぶとスレッドを止めてしまうので,もちろんこれをメインスレッドで呼ぶことはできません.Atomics.wait() はワーカスレッドからつまり Web Worker 内で呼ばなければならないという仕様があります.メインスレッドで呼ぶと待たず即座に例外を投げます.

// 共有メモリバッファをつくり,ワーカスレッド(dedicated Web Worker)を生成して渡す
const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 4);
const buffer = new Int32Array(shared);

const worker = new Worker('worker.js');
worker.postMessage(buffer);

setTimeout(() => {
    // 最初の要素に 42 を保存
    Atomics.store(buffer, 0, 42);
    // 最初の要素が変更されたことを別スレッドに通知
    Atomics.notify(buffer, 0, 1);
}, 3000);
  • ワーカスレッド(worker.js)
onmessage = event => {
    const buffer = event.data;
    const start = Date.now();

    // 5秒タイムアウトで同期的に待つ.この間ワーカスレッドは停止する
    const result = Atomics.wait(buffer, 0, 5000);
    console.log('slept', Date.now() - start, 'ms:', result);

    const val = Atomics.load(buffer, 0);
    console.log(val); // => 42
};

Vim をワーカスレッドで動かす

Vim をワーカスレッドで動かすにあたって,C 側のコードの修正はまったく必要ありませんでした.emcc-o vim.js のように出力に JavaScript ファイルを指定すると,自動で WebWorker 内で動かす前提のコードを吐いてくれます.後は new Worker('vim.js') のように読み込めば Wasm ファイル(vim.wasm)を読み込んでくれます.

JavaScript 側はメインスレッドで動かすスクリプトとワーカスレッドで動かすスクリプトに分ける必要があります.

メインスレッド側は

  • ワーカスレッドから描画情報を受け取って <canvas> に描画する処理
  • キー入力を受け取って共有バッファに書き,ワーカスレッド側に通知する
  • ワーカを生成してスタートする

ワーカスレッド側は

  • メインスレッド側から通知を受け取って共有バッファから入力を読み,Vim (Wasm) 側に伝える
  • Vim (Wasm) 側から描画情報を受け取り,メインスレッド側に通知する

という処理を行います. vim.wasm/wasm/main.ts がメインスレッド側,vim.wasm/wasm/runtime.ts がワーカスレッド側です.

全体の構成のイメージは下図のようになっています.

全体の構成イメージ

Wasm 側は Atomics.wait で待っている間停止するので,ワーカスレッド側の onmessage コールバックが発火せず,共有メモリバッファで値をやりとりする必要があります. 逆にワーカスレッドからメインスレッドに値を渡す時は postMessage() で値を渡せます.

キー入力が発生しない時と発生するときの処理のシーケンスは下図のようになっています.

入力待ちの実行シーケンス

キー入力が発生しない時 (上段,first tick)

  1. Vim (Wasm) が起動して入力待ち状態になると,最終的に gui_mch_wait_for_chars() が呼ばれ,その中で JavaScript 側に処理を渡します(vimwasm_wait_for_event())
  2. vimwasm_wait_for_event() 内で Atomics.wait() を使って共有メモリへのメインスレッドからの変更通知を同期で待ちます
  3. 今回は通知が来ず,Atomics.wait()タイムアウトしました
  4. そのまま vimwasm_wait_for_event() を抜け,Vim (Wasm) に処理を戻します

キー入力が発生する時 (下段,next tick)

  1. vimwasm_wait_for_event() 内で Atomics.wait() を使って共有メモリへのメインスレッドからの変更通知を同期で待ちます
  2. キー入力が発生しました. KeyEvent をメインスレッド側で受け取って共有メモリバッファにキー入力情報(押されたキーの名前やキーコード,モディファイアキー情報など)を書きます
  3. 共有メモリバッファのうちの1バイトを通知用の領域とし,そこに Atomics.store() でキー入力情報を書いたことを知らせる値を書いてから,ワーカスレッド側に Atomics.notify() を通じて通知します
  4. ワーカスレッド側は Atomics.wait() で待っておき,通知が来たら共有メモリバッファを見に行ってキー入力情報を得ます
  5. キー入力情報を元に Vim に与えるキーシーケンスを計算し,Vim 側に JavaScript to Wasm API を使って渡します
  6. Vim (Wasm) は受け取ったシーケンスを入力バッファに加えます
  7. Vim (Wasm) は入力バッファから入力シーケンスを読んで処理し,結果として描画イベントが発生した時は適宜 JavaScript を通じてメインスレッド側に通知します.メインスレッドは受け取った描画イベントをアニメーションフレームで <canvas/> に描画します

本当は Atomics.wait() で待つ前や Vim の処理中にキー入力がバッファに書き込まれた場合など考えることが他にもあります.

今回の実装の結果

良くなった点

今回の実装の結果,Emterpreter を使わず,全ての Vim のコードが Wasm にコンパイルされてブラウザ上で動かすことができるようになりました.

メンテ性の向上

emterpretify する関数のリストを持つ必要がなくなり,メンテがしやすくなりました.以前はクラッシュする関数を見つけてはリストを更新する作業があり,upstream に追従した時に削除された関数や追加された関数を反映させないといけなかったのが解消されました

プログラムの安定性が向上

特に,たびたび報告されていた JavaScript 側から emterpretify された C の関数を呼ぶ時にアサーションエラーや不正メモリアクセスエラーがなくなりました.また,文字列(char *)を C 側に渡そうとするとクラッシュする問題が解消されました

バイナリサイズの削減

emterpretify のバイトコードが無くなり全て Wasm になったのと,Emterpreter 本体のコードが必要なくなったので,トータルのバイナリサイズがかなり小さくなりました.gzip 圧縮で,

  • Before: 1025.49 KB
  • After: 453.308 KB

と半分以下になりました.

ビルド速度

Emterpreter のための変換を行わなくて良くなったため,emcc によるビルド速度が向上しました.

  • Before:
emcc  48.25s user 3.55s system 309% cpu 16.745 total
  • After:
emcc  4.90s user 0.51s system 112% cpu 4.792 total

数倍速くなってますね.

ユーザとのインタラクション

Vim 本体はワーカスレッドで実行されているため,メインスレッドで処理されるユーザの入力処理(スクロールやキー入力など)を Vim がブロックしないようになりました.

実行速度

以前はユーザの入力を emscripten_sleep() で 10ms ごと polling しながら待っていたため,最大で 10ms の遅延がありました. また,10ms ごとに Emterpreter の suspend/resume が起きていたため,おそらくそれも実行を遅くしていたと思います(未確認).

10ms ごとのポーリングをやめ,ユーザからの入力を Atomics.wait() で待つため,メインスレッドから Atomics.notify() されてからの遅延は無くなり,少なくともその分速く実行できるようになりました.(比べないと分からないレベルですが…)

悪くなった点

Chrome (と Chromium ベースのブラウザ)以外ではデフォルトで動かなくなりました.これは Spectre 脆弱性によって SharedArrayBufferAtomics API が一時的に多くのブラウザで無効にされているためです.FirefoxSafari ではブラウザのフィーチャフラグを立てることで動かすことができます.

今後

  • 今は Web Worker を使っていますが,Wasm のマルチスレッド対応が使えるかもしれないと思っています.同時に i32.atomic.wait などのアトミック命令も追加されているので,JavaScriptAtomics API の代わりに使うことで,Wasm と JavaScript 間のインタラクションを無くしてワーカスレッド側の入力待ち処理を Wasm で完結させられるかもしれません
  • 今は <canvas/> をメインスレッド側で描画していますが,ワーカスレッド側でキャンバスを描画してメインスレッド側に転送する OffscreenCanvas を使って描画処理をワーカスレッド側に持ってくることができるかもしれません
  • upstream の追従作業
  • 'small' や 'normal' feature でビルドできるようにする
  • ファイル保存や読み込み,クリップボード対応など

感想

最初に Atomics API を見た時は JavaScript で非同期処理ライブラリをつくるような人以外は用は無さそうだなと正直思っていたんですが,まさか自分が使うことになるとは思いませんでした…

Emterpreter の動作が不安定すぎる問題で長らく詰まってしまっていたのですが,ようやく次のステップに進めそうで良かったです.

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 では現在のカーソル位置からポップアップを出すのに十分な幅・高さがあるかを確認し,カーソルの上下左右にフロートウィンドウを出し分けます.