目黒.vim #16 に参加して Vim script に const を実装するパッチを書いた
あいにくの雨でしたが,Meguro.vim #16 に参加しました.
「この雨の中 Vim のもくもく会に来た者たちだ.面構えが違う」 #megurovim
— ドッグ (@Linda_pp) June 15, 2019
目黒.vim は Vim に関したり関しなかったりする作業をするもくもく会で,今回は Vim に :const
を追加するパッチを書きました.
: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
で定義した変数のロックを解除することが可能です- JavaScript の
const
はスコープがレキシカルですが,Vim script の:const
はスコープがダイナミックのままです - JavaScript 処理系はコードのパース時に
const
定義された変数への再代入を検知してエラーにできますが,Vim スクリプト処理系は素朴なインタープリタなので:const
で定義された変数が実際に書き換えられようとするまでエラーになりません
:const
コマンドの実装
diff はこちら. src/eval.c
に ex_const()
を定義し, src/ex_cmds.h
に追加したいコマンドを定義して ex_const
関数ポインタを登録しておくと,:const
で ex_const()
が呼ばれます.
ちなみに新しいコマンドを追加した時は make cmdidxs
でコマンド検索表である src/ex_cmdidxs.h
を再生成する必要があります.
:const
は変数をロックする以外は :let
と同じ挙動のため,実装を :let
と共有しています.定義した変数をロックするかどうかのフラグを :let
の実装関数に追加し,フラグが立っている場合は変数定義時に di_flags
の DI_FLAGS_LOCK
フラグと di_tv.v_lock
の VAR_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
し,:Termdebug
で Vim の中で Vim を起動してgdb
を使ってデバッグする.Mac でgdb
を使うには実行ファイルをコード署名する必要があり若干面倒ですが,大変便利です
vim/vim へのプルリク
ドキュメントを書いて,パッチを GitHub の vim/vim リポジトリにプルリクします.
新機能の実装の場合,取り込まれるかどうかは作者の Bram さん次第なのでどうかなと思ったのですが,サクッとマージしてもらえました(Vim では変更をパッチとして取り込むので,GitHub のマージ機能を使っていません).Bram さんは普段 JavaScript や TypeScript を書くらしく,同じことを考えたことがあったらしいです.Vim script の新機能を Bram さんに説明したいときは JavaScript を引き合いに出すと良いのかもしれません.
あまりに速くマージされたので,いくつか細かいミスに後から気付き追加のパッチを送りました.
Vim on Wasm on Web Worker on Browser with Atomics
この記事は以前の
の続編で,WebAssembly (Wasm) にポーティングした Vim の話です.
TLDR
Wasm にコンパイルした Vim のコードを Web Worker(ワーカスレッド)の中で動かすことで,メインスレッドで行われるユーザのインタラクションをエディタがブロックしなくなりました.
また,イベントループのポーリングを Atomics.wait()
でやってキー入力を共有メモリバッファで受け取ることで Emterpreter を捨て,実行速度・安定性・バイナリサイズ・ビルド時間・メンテ性が向上しました.
実装:
これまでの問題点
前回の記事では 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()
のように同期的に待つ関数のように使えますが,実際は一旦処理を中断して非同期に待つ処理に変換されます.emcc
(emscripten の C コンパイラ)は emscripten_sleep()
を使っている関数を直接 Wasm にコンパイルせず,Wasm の上で動くインタープリタ (Emterpreter) で実行するためのバイトコードに変換します (emterpretify).実行時にはそのバイトコードをインタープリタで実行し,emscripten_sleep()
の呼び出し箇所で一旦実行状態を保存(suspend)して JavaScript 側の setTimeout()
を読んで実行を中断します.タイマーがコールバックを呼んだら Wasm 側に戻り,インタープリタはその中で保存しておいた実行状態を復帰(resume)して実行を再開します.
このような力技により前回はどうにか Vim を動かすことに成功しました.ですが,前回の記事にも書いたように,次の問題が残りました
- Emterpreter で実行される関数は Wasm から直接呼び出すことができません(逆に Emterpreter から Wasm の関数は呼べる).なので,
emscripten_sleep()
を呼んでいる関数を呼んでいる関数,それを呼んでいる関数,... というふうに呼び出し元の関数も Wasm には直接コンパイルできず,Emterpreter のバイトコードにコンパイルする必要があります.Vim でsleep()
が必要な関数(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^ ̄
なんと,無いと思っていた JavaScript の sleep()
がこんなところにありました.
ですが,これを呼ぶとスレッドを止めてしまうので,もちろんこれをメインスレッドで呼ぶことはできません.Atomics.wait()
はワーカスレッドからつまり Web Worker 内で呼ばなければならないという仕様があります.メインスレッドで呼ぶと待たず即座に例外を投げます.
- メインスレッド(
<script>
で読まれるスクリプト)
// 共有メモリバッファをつくり,ワーカスレッド(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/wasm/main.ts
がメインスレッド側,vim.wasm/wasm/runtime.ts
がワーカスレッド側です.
全体の構成のイメージは下図のようになっています.
Wasm 側は Atomics.wait
で待っている間停止するので,ワーカスレッド側の onmessage
コールバックが発火せず,共有メモリバッファで値をやりとりする必要があります.
逆にワーカスレッドからメインスレッドに値を渡す時は postMessage()
で値を渡せます.
キー入力が発生しない時と発生するときの処理のシーケンスは下図のようになっています.
キー入力が発生しない時 (上段,first tick)
- Vim (Wasm) が起動して入力待ち状態になると,最終的に
gui_mch_wait_for_chars()
が呼ばれ,その中で JavaScript 側に処理を渡します(vimwasm_wait_for_event()
) vimwasm_wait_for_event()
内でAtomics.wait()
を使って共有メモリへのメインスレッドからの変更通知を同期で待ちます- 今回は通知が来ず,
Atomics.wait()
がタイムアウトしました - そのまま
vimwasm_wait_for_event()
を抜け,Vim (Wasm) に処理を戻します
キー入力が発生する時 (下段,next tick)
vimwasm_wait_for_event()
内でAtomics.wait()
を使って共有メモリへのメインスレッドからの変更通知を同期で待ちます- キー入力が発生しました.
KeyEvent
をメインスレッド側で受け取って共有メモリバッファにキー入力情報(押されたキーの名前やキーコード,モディファイアキー情報など)を書きます - 共有メモリバッファのうちの1バイトを通知用の領域とし,そこに
Atomics.store()
でキー入力情報を書いたことを知らせる値を書いてから,ワーカスレッド側にAtomics.notify()
を通じて通知します - ワーカスレッド側は
Atomics.wait()
で待っておき,通知が来たら共有メモリバッファを見に行ってキー入力情報を得ます - キー入力情報を元に Vim に与えるキーシーケンスを計算し,Vim 側に JavaScript to Wasm API を使って渡します
- Vim (Wasm) は受け取ったシーケンスを入力バッファに加えます
- 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 脆弱性によって SharedArrayBuffer
や Atomics
API が一時的に多くのブラウザで無効にされているためです.Firefox や Safari ではブラウザのフィーチャフラグを立てることで動かすことができます.
今後
- 今は Web Worker を使っていますが,Wasm のマルチスレッド対応が使えるかもしれないと思っています.同時に
i32.atomic.wait
などのアトミック命令も追加されているので,JavaScript のAtomics
API の代わりに使うことで,Wasm と JavaScript 間のインタラクションを無くしてワーカスレッド側の入力待ち処理を Wasm で完結させられるかもしれません - 今は
<canvas/>
をメインスレッド側で描画していますが,ワーカスレッド側でキャンバスを描画してメインスレッド側に転送するOffscreenCanvas
を使って描画処理をワーカスレッド側に持ってくることができるかもしれません - upstream の追従作業
- 'small' や 'normal' feature でビルドできるようにする
- ファイル保存や読み込み,クリップボード対応など
感想
最初に Atomics
API を見た時は JavaScript で非同期処理ライブラリをつくるような人以外は用は無さそうだなと正直思っていたんですが,まさか自分が使うことになるとは思いませんでした…
Emterpreter の動作が不安定すぎる問題で長らく詰まってしまっていたのですが,ようやく次のステップに進めそうで良かったです.
Neovim のフロートウィンドウ機能を使って git-messenger.vim をつくりなおした
git-messenger.vim は,カーソル下の行のコミット情報を表示する Vim プラグインです.
他所のプロジェクトのコードや 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 以降では前述のフロートウィンドウで,それ以外ではプレビューウィンドウで実装されています.その後カーソルを動かすとポップアップは自動で閉じます.
基本的にはこれだけですが,ポップアップを開いたあとにカーソルを動かさずそのままもう一度 :GitMessenger
コマンドか <Leader>gm
を入力するとポップアップウィンドウの中にカーソルを移動できます.コミットメッセージが長すぎて全部表示できなかった時のスクロールやメッセージのクリップボードへのコピーなどができます.
ウィンドウ内ではいくつかローカルなマッピングが定義されており,o
でより古いコミットを手繰ることができます.直近のコミットにほしい情報が書いてあるとは限らない(例えばフォーマッタでの整形が挟まったり,別のリファクタリングが挟まったりなど)ので,そういうときはより古いコミットのメッセージにほしい情報があることがあります.さらに O
で新しいコミットに戻ったり,q
でウィンドウを閉じたりできます(?
でヘルプ)
カスタマイズ
- デフォルトのマッピング
<Leader>gm
が気に入らない場合はg:git_messenger_no_default_mappings
にv:true
をセットして<Plug>(git-messenger)
をマップすれば OK です - 他にもいくつか
<Plug>
マップが定義されています - いくつかの挙動はグローバル変数で制御できるようになっています
- Neovim のみポップアップウィンドウ内のハイライトをカスタマイズできます.デフォルトの色合いがお使いのカラースキームに合わないときは自分で色をカスタマイズできます.Neovim のみなのはウィンドウローカルにハイライト色を変更できる
winhighlight
というオプションを Neovim だけが持っているためです
詳しくはリポジトリの README.md か :help git-messenger
で確認できます.
Neovim のフロートウィンドウについて
Vim のウィンドウは分割することでタイル型に配置されるレイアウトしか対応しておらず,ウィンドウが重なるようなレイアウトは(無理矢理バッファを書き換えてそれっぽく見せるようなことをしない限り)実現できませんでした.
そこで Neovim では CSS の position: relative
や position: absolute
のように位置基準でウィンドウの重なりを許すウィンドウレイアウトを実装しました.
何が良いのか
この機能の設計で特に優れていると感じる点は,レイアウト以外はほぼ完全に普通の Vim のウィンドウと同じところです.
- CUI で使える
- プラグイン開発者はウィンドウの開き方だけ分かれば,あとは普通の Vim のウィンドウと同様に扱える
- ウィンドウ内のコンテンツは普通の Vim のバッファなので,
setline()
などで自由に追加・変更・削除できる - filetype をセットしたり,自由にハイライトできる
- ウィンドウを開く以外はバルーンのような専用の API が要らない
この機能により,プラグイン開発者はウィンドウの上にオーバーレイするような UI を変なハックをしたり妥協することなく実装することができます. ぱっと思いつくのは
などに使えそうです.
使い方
フロートウィンドウを開くのには 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()
でウィンドウのレイアウトをやりなおすことができるので,これを使ってウィンドウのサイズ変更や位置変更をします.
ちなみに row
,col
,width
,height
がエディタのスクリーンからはみ出してしまってもエラーになりません.Neovim はなるべくウィンドウをスクリーン内に収めるように描画します.git-messenger.vim では現在のカーソル位置からポップアップを出すのに十分な幅・高さがあるかを確認し,カーソルの上下左右にフロートウィンドウを出し分けます.