🍣

Vimプラグインを書く言語について と宣伝

2021/06/17に公開

はじめに

Rust 1.53が本日リリースされ、rustdocが吐く目次データのフォーマットが変わりました。これによりdocを高速に検索するための自作ツールrustdoc-index[1][2]がstableでも使えるようになりました。
rustdoc-index

ここからが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]

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を表示できます。(補完は自作でない)
virtualsnip

実行可能ファイルだけでなく動的ライブラリについても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のツールはここに書いた以外にも探せば選択肢があります。

vim9 script

neovimがluaに力を入れているのに対してvimが力を入れているのはvim9スクリプトです。
Vim9 script はどれくらい速いのか

vim/neovim両方からluaより速い言語が使えたらここまでのPros/Cons全てなかったことにして採用できるんだけどその未来はまだこないので厳しい。

lsp

プラグインをUIとコアに分離する試みです。UIはすでに実装があるのでlspサーバだけを実装すればよく、さらにlspサーバ実行までをlspクライアント側に任せることができ任意のプログラミング言語を使用できます。

lspはテキスト編集やエラーの表示などコーディングに必要なメソッドをUIとコア間のインターフェイスとして提供しますが、これを曲解/悪用すれば例えばテキストベースwebブラウザをlspサーバとして提供することなんかも考えることができます。
web-browser-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/wasmer2.0を迎えました。wasmは整数型以外のデータやりとりについてメモリの使い方について自由なので各自で決めなければなりません。バイナリフォーマットで無理やり[17]やりとりすることもできますが、2.0でモジュール間参照の導入といっているのでもしかしたらやりとりしやすくなるのかもしれません(不明)。

"vimプラグインの安全性はソースがオープンであることにより担保されている"はずなのでwasmが提供する機能認可ベース安全性は不要ですし、むしろいまのところマルチスレッドな処理を書けない点が気になっています。

まとめ

  • Vim Script
  • system
  • libcall
  • jobstartjob_start
  • Python/Ruby Remote Plugin
  • Lua vim.loop (libuv)
  • vim9 script
  • lsp
  • terminal
  • denops

を紹介しました。トレードオフとしての実行速度, データシリアライズ転送速度, コンパイル, 環境構築を受け入れれば、vimプラグインは好きな言語で書くことができます。vim-jpのコミュニティは大きいので、私のようにvim scriptを書きたくなくて7年間コピペだけでvimを使うこともできますが、お好きな\overset{\scriptsize \text{programming language}}{\small \text{問題解決ツール}}\overset{\scriptsize \text{vim}}{\small \text{人生}}を改善しましょう。

脚注
  1. homeのstd等とローカルのtarget/docのアイテム一覧をstdoutに出力するコマンドがrustdoc-indexリポジトリで約300ms ↩︎

  2. rhysd/rust-doc.vim をインスパイアしたものでそれよりも速い ↩︎

  3. https://vim-jp.org/slacklog/C011FPVKT2R/2021/04/ ↩︎

  4. 有名実装から例を挙げると hrsh7th/vim-vsnipsession.vim MIT hrsh7thさんが書くコードは読みやすい ↩︎

  5. なんかlint弱いしつらくない? 動的型付け言語のドキュメントは自然言語で記述されているのがしんどい ↩︎

  6. 数値は実装途中にみた値 ↩︎

  7. octaltree/virtualsnipではここでデータをjson[18]にしてrustに計算を投げている ↩︎

  8. neovimはmsgpack-rpcが使えるがvimにはないのでjson_encodeを使う ↩︎

  9. 時系列的には正しくないがこの記事ではこれ以降がvim/neovim互換がない話 ↩︎

  10. virtualsnipははじめvim scriptとluaで書いていたが書いている間が一番virtualsnipの機能がほしかった ↩︎

  11. https://github.com/vim-jp/issues/issues/48 ↩︎

  12. Vimにたくさんあるファジーファインダー系プラグインを比較してみる ↩︎

  13. 私が参加するより以前から ↩︎

  14. pythonはpython3でまともになりcを使役して成りがった ↩︎

  15. https://github.com/denoland/deno/issues/8490 ↩︎

  16. READMEに16言語名前が上がっているがもちろんlua, vim scriptはない ↩︎

  17. protobufにしてメモリに載せて送る octaltree/wasmer-protobuf-example ↩︎

  18. json_encodeはvim/neovimどちらでも使える。neovimではmsgpack-rpcが採用されてるがvimにはないしmsgpack_encodeだけの提供はされていない(?) ↩︎

Discussion