Vimプラグインを書く言語について と宣伝
はじめに
Rust 1.53が本日リリースされ、rustdocが吐く目次データのフォーマットが変わりました。これによりdocを高速に検索するための自作ツールrustdoc-index[1][2]がstableでも使えるようになりました。
ここからがvimの話です。私がvim-jpのslackコミュニティに参加したのはこのツールの宣伝が目的[3]でした。そこはvim猛者がたくさんいて、2021/04/01に参加してたった2ヶ月で、7年テキトーに使っていたvimについて学ぶことが多くありました。細々とした知見を書き連ねるのはまとめきれないので、この記事ではvimプラグインとプログラミング言語と題して書きます。結論からいうとトレードオフを理解した上で任意の言語を使うことができます。
Vim Script
Vim scriptでプラグインを作ろう 〜 Vimはいいぞ!ゴリラと学ぶVim講座(8)
つくり方についてはきれいな記事や有名実装をご参照ください。
構文や変数スコープの明示などクセの強い言語です。変数スコープの明示によりファイル単位でカプセル化しオブジェクト指向をすることも可能です[4]。公式言語なのでvim/neovimのapiを呼ぶのは強いが、処理速度が速くないというのが一番の短所です。遅いこと動的型付け言語であることに目を瞑ればツールは一通りあります[5]。
- タスクランナ GNU Makefile
- vimが提供しない標準ライブラリ相当を補う vim-jp/vital.vim
- コード補完 Shougo/neco-vim
- 静的解析 Vimjas/vint
- テスト thinca/vim-themis
- ドキュメント /doc/{pluginame}.txtに書く
- コード整形ツールは見つからなかった
vim scriptからバイナリを呼び出す
vim/neovimではsystem
という関数が提供されており実行可能ファイルを同期的に(vim scriptの処理を一旦止めて)呼び出すことができます。これにより状態をvim scriptに持ち、標準入出力を介し重い計算だけを速い言語に任せることができます。データ移動コストがかかります。コンパイラ言語を使う場合は手動でコンパイルするかgithubにビルドを置くかプラグインマネージャによる自動ビルド機能を使う必要があり実行が速い分導入時の短所を持ちます。
私が好きなコンパイラ言語としてRust
- 静的型付け言語
- 優秀なパッケージマネージャで好きなライブラリを使える
- コード補完&lsp rust-analyzer/rust-analyzer
- 静的解析 rust-lang/rust-clippy
- テスト cargo test
- ドキュメント cargo doc
- コード整形 cargo fmt
vim scriptで250msかかるスニペット一覧のパースがrustではデータ移動含めて15msでできる[6][7]ので1文字入力ごとに処理してリアルタイムにvirtualtextを表示できます。(補完は自作でない)
実行可能ファイルだけでなく動的ライブラリについてもvim/neovimでlibcall
関数を使うことができます。
libcallの使い方まとめ - Qiita
使用者が多いであろうShougo/vimproc.vimからは動的ライブラリのマルチプラットフォーム対応の苦労が伝わってきます。
vimprocは非同期に実行可能ファイルを呼び出すためのライブラリです。以前は同期的に実行するsystem
を使うかvimprocで非同期に実行するかしかありませんでしたが、vim本体に非同期実行機能が実装されました。vimと並行してプロセスが走り続けることができるので状態を持つことができます。標準入出力を介し[8]てvim scriptとやりとりができます。
ここから[9]がvim/neovim両対応地獄の始まりです。vimからフォークしたneovimは同じapiを提供するわけではなく、vimではjob_start
, neovimではjobstart
という引数も返り値も異なる関数が実装されており、両方対応する必要があります。
また、neovimの場合はmsgpack-rpcを持っているのでvim scriptとやりとりせずとも非同期に走るプロセスからneovimサーバのapiを直接叩くことができます。
Python/Ruby/Lua
前述のvim scriptから呼び出す方法でもpython/ruby/luaのインタプリタを呼び出すことはできますが、vim/neovimはこれらのインタプリタを使ってスクリプトを直接実行できます。ただしvimのコンパイル時に機能を有効にする必要があります。また、各言語からvim/neovimに対するapiが同一ではありません。
Lua
vim scriptと似ているがクセの薄い構文を持ちます。それ故に両方書いていると混乱します[10]。python/ruby/luaの中でもneovimがluajitに力を入れていて、neovimのlspクライアントはluaで書かれています。このlspクライアントはlspサーバを非同期に実行するためにvim.loop.spawnというapiを使っています。これはluvit/luvというluaのライブラリをneovimが組み込んで提供しているものです。
luaはjitを有効にしていなくてもvim scriptやpythonより速い[11]ので、vim scriptの代わりにluaで処理を行いapiが一致しないjobstart
, job_start
の代わりにこのluvを使うことを考えることができます。luaのライブラリが使えればjson_encode
でなくともmsgpackやprotobufなどのバイナリフォーマットでデータをやりとりすることまでできます。これらはluarocksというパッケージマネージャで提供されています。これをvim/neovim両方から使えるようにしたのがoctaltree/vimrocksです。luaを有効にしたvimを用意する必要やluaからvim/neovimに対するapiが一致しない短所は解決しませんがluaを便利に使うことができます。
luaのツールはここに書いた以外にも探せば選択肢があります。
- ライブラリ luarocks
- ドキュメント /doc/{pluginame}.txtに書く
- 静的解析 mpeterv/luacheck
- コード整形 Koihik/LuaFormatter
- vim/neovim両方のテスト thinca/vim-themis
vim9 script
neovimがluaに力を入れているのに対してvimが力を入れているのはvim9スクリプトです。
Vim9 script はどれくらい速いのか
vim/neovim両方からluaより速い言語が使えたらここまでのPros/Cons全てなかったことにして採用できるんだけどその未来はまだこないので厳しい。
lsp
プラグインをUIとコアに分離する試みです。UIはすでに実装があるのでlspサーバだけを実装すればよく、さらにlspサーバ実行までをlspクライアント側に任せることができ任意のプログラミング言語を使用できます。
lspはテキスト編集やエラーの表示などコーディングに必要なメソッドをUIとコア間のインターフェイスとして提供しますが、これを曲解/悪用すれば例えばテキストベースwebブラウザをlspサーバとして提供することなんかも考えることができます。
terminal
vim/neovimがそれぞれ持つterminal上でTUIアプリケーションを動かすことができます。vimのバッファを触るのとは操作感が異なるのが短所です。
ちなみにfuzzy finder[12]は、バイナリ動かして描画に必要な分だけvim scriptに持ってくる実装やfzfなどのTUIを用いたものが体感で速い
vim-denops/denops.vim
vim-jpでは最近[13]盛り上がっているものです。vim/neovimの差を吸収したapiを提供しdenoでプラグインを書けるものだと思います。vim-jpコミュニティは強いので長いものには巻かれろという利点があります。
- ライブラリ パッケージマネージャの操作必要なくgithub上のファイルをインポートできる
- ドキュメント deno doc
- 静的解析
deno test --unstable --no-run -A **/*.ts
で型チェックとdeno lint - コード整形 deno fmt
- テスト deno test
主観でのdenoの短所として以下のものが挙げられます。
- wasmランタイムに力入れているが、CやRustのライブラリがまだ全然ダメ[14][15]
- typescriptで型ヒント書いてもコンパイラ言語より遅い luajitと比べたことはないので不明
- 静的型付け言語はなんでも良いが型ヒントの信頼できなさ
- typescriptよりrustのが簡単
おまけ wasmer 2.0
ローカルで様々な言語から動かすことのできる[16]wasmランタイムwasmerio/wasmerが2.0を迎えました。wasmは整数型以外のデータやりとりについてメモリの使い方について自由なので各自で決めなければなりません。バイナリフォーマットで無理やり[17]やりとりすることもできますが、2.0でモジュール間参照の導入といっているのでもしかしたらやりとりしやすくなるのかもしれません(不明)。
"vimプラグインの安全性はソースがオープンであることにより担保されている"はずなのでwasmが提供する機能認可ベース安全性は不要ですし、むしろいまのところマルチスレッドな処理を書けない点が気になっています。
まとめ
- Vim Script
system
libcall
-
jobstart
とjob_start
- Python/Ruby Remote Plugin
- Lua
vim.loop
(libuv) - vim9 script
- lsp
- terminal
- denops
を紹介しました。トレードオフとしての実行速度, データシリアライズ転送速度, コンパイル, 環境構築を受け入れれば、vimプラグインは好きな言語で書くことができます。vim-jpのコミュニティは大きいので、私のようにvim scriptを書きたくなくて7年間コピペだけでvimを使うこともできますが、お好きな
-
homeのstd等とローカルのtarget/docのアイテム一覧をstdoutに出力するコマンドがrustdoc-indexリポジトリで約300ms ↩︎
-
rhysd/rust-doc.vim をインスパイアしたものでそれよりも速い ↩︎
-
有名実装から例を挙げると hrsh7th/vim-vsnipのsession.vim MIT hrsh7thさんが書くコードは読みやすい ↩︎
-
なんかlint弱いしつらくない? 動的型付け言語のドキュメントは自然言語で記述されているのがしんどい ↩︎
-
数値は実装途中にみた値 ↩︎
-
octaltree/virtualsnipではここでデータをjson[18]にしてrustに計算を投げている ↩︎
-
neovimはmsgpack-rpcが使えるがvimにはないので
json_encode
を使う ↩︎ -
時系列的には正しくないがこの記事ではこれ以降がvim/neovim互換がない話 ↩︎
-
virtualsnipははじめvim scriptとluaで書いていたが書いている間が一番virtualsnipの機能がほしかった ↩︎
-
私が参加するより以前から ↩︎
-
pythonはpython3でまともになりcを使役して成りがった ↩︎
-
protobufにしてメモリに載せて送る octaltree/wasmer-protobuf-example ↩︎
-
json_encode
はvim/neovimどちらでも使える。neovimではmsgpack-rpcが採用されてるがvimにはないしmsgpack_encode
だけの提供はされていない(?) ↩︎
Discussion