はじめに
NeovimはUIと編集エンジンが分離されており、Msgpack-RPCを通じて外部UIから操作できます。それなら、Neovim本体をWebAssemblyへコンパイルし、ブラウザ内で起動できれば、サーバー上のNeovimへ接続するのではなく、ブラウザだけでNeovimを操作できるはずです。
そこで、NeovimをWebAssembly System Interface(WASI)向けにビルドし、ブラウザ上で実行するためのnvim-wasmを作成しました。
NeovimのメンテナーであるJustin M. Keyesさんが、Xでnvim-wasmのリポジトリとデモを紹介してくれました!
https://x.com/justinmk/status/1999312671817322729
リポジトリはこちらです。
最小構成のデモはこちらです。
最小構成のデモでは、ブラウザ上でNeovimを起動し、通常のNeovimと同じようにモードを切り替えて文字を入力できます。
この記事では、NeovimをWasm化するために必要だった処理と、ブラウザから操作する仕組みについて紹介します。
概要
nvim-wasmは、Neovimをwasm32-wasi向けにクロスコンパイルするビルドラッパーと、生成したWasmをブラウザで操作するデモをまとめたリポジトリです。
生成物は次のWASIモジュールです。
build-wasm/bin/nvim
これはホストOSで直接起動する実行ファイルではありません。ブラウザではWeb Worker内のWASIランタイムへ読み込み、仮想ファイルシステムと標準入出力を接続して起動します。
リポジトリには、表示方法と入力方法が異なる4つのデモを用意しています。
| デモ | 表示方法 | 入力方法 | 公開デモ |
|---|---|---|---|
examples/demo | DOMでlinegridを描画 | SharedArrayBuffer | 開く |
examples/demo-asyncify | DOMでlinegridを描画 | postMessageとAsyncify | 開く |
examples/demo-monaco | Monaco Editorへバッファとカーソルを反映 | SharedArrayBuffer | 開く |
examples/demo-xterm | linegridをANSIへ変換し、ghostty-webで描画 | SharedArrayBuffer | 開く |
いずれも、ブラウザ風に再実装したVimではありません。Web Worker上で起動したNeovimへキー入力を送り、Neovimが返す状態を画面へ描画しています。
仕組み
WASI向けにクロスコンパイルする
ビルドにはWASI SDKを使用します。CMakeのツールチェーンファイルでターゲットをwasm32-wasiに設定し、WASI SDKのClang、sysroot、ar、ranlibを指定しています。
Neovimが使用するLua、libuv、luv、lpeg、Tree-sitter、utf8proc、unibiliumもWASI向けの静的ライブラリとしてビルドし、最後にNeovimへリンクします。
WASI SDK、CMake、Binaryenなどは.toolchainsへダウンロードされるため、システム全体へ個別にインストールする必要はありません。
コード生成にはホスト側のLuaを使う
Neovimのビルドでは、途中でLuaを実行してソースコードを生成します。しかし、クロスコンパイル中に生成したWasm版のLuaは、ビルドを実行しているLinux上で直接起動できません。
そのため、最初にホスト上で動くLuaとnlua0をビルドします。
make host-lua
Wasm版Neovimのビルド中にコード生成が必要になると、scripts/build/host_lua_gen.luaを経由してホスト版Luaを実行します。Wasmへ変換するプログラムと、ビルド中に実行するプログラムを分けることで、クロスコンパイルを成立させています。
WASIにない機能を補う
Neovimとlibuvは、通常のUnix環境にあるPTY、signal、pthread、ioctlなどを使用します。一方、WASIの実行環境では同じ機能をそのまま利用できません。
nvim-wasmでは、WASI向けのヘッダーとstubをpatches/wasi-shimに用意しています。また、libuvとluvへ必要な変更を適用し、WASIで使える処理と、ENOSYSを返す未対応処理を分けています。
Neovimのサブモジュール内は変更せず、CMakeのoverrideと外部patchからソースの差し替えやコンパイルオプションの追加を行います。これにより、参照するNeovimを更新したときも、WASI固有の変更点を追いやすくしています。
Web WorkerでNeovimを起動する
NeovimはWeb Worker内で起動します。メインスレッドは画面描画とブラウザの入力を担当し、Neovimの処理はWorker側で実行します。
ブラウザには通常のOSと同じファイルシステムがないため、Neovimのruntime、WASI向けにビルドしたライブラリ、バージョン情報をtar.gzへまとめて配布します。Workerは起動時にこれを仮想ファイルシステムへ展開します。
tar -czf examples/demo/nvim-runtime.tar.gz \
-C neovim/.. runtime \
-C build-wasm usr nvim_version.lua
Msgpack-RPCでNeovimと接続する
Neovimは--embedで起動し、標準入出力をMsgpack-RPCの通信路として使用します。
メインスレッドはnvim_ui_attachを呼び出し、ext_linegridのredraw通知を受け取ります。通知には文字、ハイライト、カーソル位置、モードなどが含まれます。最小構成のデモでは、その内容をDOMのグリッドへ直接反映します。
キーボード入力はnvim_inputとしてNeovimへ送ります。画面だけをVim風に描画するのではなく、入力の解釈、モードの遷移、バッファの編集はNeovim自身が行います。
ブラウザのキー入力
└─ nvim_input
└─ Web Worker上のNeovim
└─ redraw通知
└─ DOMへ描画
SharedArrayBufferを使用する
通常版のデモでは、標準入力にSharedArrayBufferのリングバッファを使用します。メインスレッドが入力を書き込み、Worker上のNeovimが読み取ります。
SharedArrayBufferをブラウザで使用するには、ページをcross-origin isolatedにする必要があります。デモを配信するサーバーでは、次のCOOP/COEPヘッダーが必要です。
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
AsyncifyでSharedArrayBufferを使わない
静的ホスティング先でCOOP/COEPを設定できない場合に備え、SharedArrayBufferを使わないデモも用意しました。
通常のWasm実行中は、Neovimが入力待ちで停止している間にWorkerがpostMessageを受け取れません。そこで、ビルド済みのWasmをBinaryenのAsyncifyで後処理し、WASIのpoll_oneoffで実行を一時停止できるようにします。
make wasm-asyncify
入力が届くまでWasmの状態を退避し、WorkerがpostMessageを受け取ると実行を再開します。この方式ではCOOP/COEPは不要ですが、通常版よりWasmのサイズが増え、実行時のオーバーヘッドも発生します。
現在リポジトリに含まれる生成物では、通常版が約7.8MB、Asyncify版が約8.4MBです。
Asyncify版の公開デモはこちらです。
応用例
Monaco Editor
Monaco版では、編集処理をMonacoへ任せていません。Monacoは読み取り専用とし、キー入力はすべてNeovimへ送ります。
nvim_buf_attachでバッファの変更を受け取り、Neovimのバッファ、カーソル、Visual selectionをMonacoへ反映します。これにより、画面はMonacoですが、編集状態はNeovimが保持します。
Monaco版の公開デモはこちらです。
ターミナルUI
ターミナル版では、ext_linegridの更新をANSIエスケープシーケンスへ変換し、xterm.js互換APIを持つghostty-webで表示します。
NeovimをPTY上で起動して端末画面を転送しているわけではありません。Neovimの外部UIプロトコルから受け取ったグリッドを、ターミナル表示へ変換しています。
ターミナル版の公開デモはこちらです。
ビルド方法
Lua 5.1互換のLuaインタープリター、curl、tarを用意します。リポジトリはNeovimをサブモジュールとして含むため、--recursiveを付けて取得します。
git clone --recursive https://github.com/MuNeNiCK/nvim-wasm.git
cd nvim-wasm
ホスト側のLua、Wasm向けの依存ライブラリ、Neovimの順にビルドします。
make host-lua
make wasm-deps
make wasm
ビルドが完了すると、次のWASIモジュールが生成されます。
build-wasm/bin/nvim
最小構成のデモを動かす場合は、runtimeとWasmを配置します。
tar -czf examples/demo/nvim-runtime.tar.gz \
-C neovim/.. runtime \
-C build-wasm usr nvim_version.lua
cp build-wasm/bin/nvim examples/demo/nvim.wasm
python3 examples/demo/serve.py
ブラウザで次を開きます。
http://localhost:8765/
serve.pyはSharedArrayBufferに必要なCOOP/COEPヘッダーを付けて配信します。Asyncify版はmake demo-asyncifyで生成し、examples/demo-asyncify/serve.pyから起動できます。
制限事項
WASIには通常のデスクトップOSと同じ機能がすべて用意されているわけではありません。現在のnvim-wasmでは、特に次の機能に制限があります。
- PTYや外部プロセスの起動
- signalやpthreadに依存する処理
- 通常のOSと同じネットワーク、ソケット、ファイル監視
- ネイティブ共有ライブラリを必要とするプラグイン
- ブラウザ外のファイルシステムへの直接アクセス
現時点では、通常のNeovimをそのまま置き換えるものではなく、Neovimの編集エンジン、Lua、Msgpack-RPC、外部UIをブラウザへ持ち込むための実装です。
今後の展望
Neovim公式のWasm対応
Neovimでは、Wasmを公式のビルドターゲットにする作業が進められています。
2025年8月に作成されたWasm対応のtracking issueでは、ブラウザ内で動作するNeovimの成果物、ビルド手順、CIを整備することが目標として挙げられています。このissueの参考実装にはnvim-wasmも掲載されています。
また、GSoC 2026のtracking issueでは、Emscriptenによる公式ビルドターゲット、ブラウザ向けRPC、SharedArrayBufferを使った標準入出力、ブラウザUIなどの実装が進められています。nvim-wasmはWASI SDKを使用していますが、Neovim本体側ではEmscriptenを使用する方針です。
GSoC 2026の成果として、Emscripten向けのビルド対応を追加するPR #40008は2026年6月にmergeされました。2026年7月30日時点では、ブラウザ上でNeovimを操作するためのPR #40772がopenになっています。
以前、GSoCへの参加を検討していたSarojさんから、nvim-wasmを参考にしてよいか連絡をいただき、了承しました。今後も、Neovim本体へのPRやGSoCの成果物で、nvim-wasmの設計や実装を参考にすることに問題はありません。
Neovim本体へのPRやGSoCの成果物で参照する場合は、ContributorsにMuNeNiCKも記載いただけると幸いです。Contributorsという欄がない場合は、Acknowledgementsなどの同等の箇所でも構いません。
vscode-neovimやFirenvimへの活用
nvim-wasmは、デモページだけでなく、Neovimをバックエンドとして利用する既存のツールにも活用できると考えています。
たとえば、vscode-neovimは、VS Codeの編集機能とNeovimを統合する拡張機能です。現在はユーザーがNeovimを別途インストールし、拡張機能がそのNeovimバイナリをバックエンドとして起動します。
vscode-neovimには、NeovimをWasmとして拡張機能へ同梱するfeature requestもあります。Wasm版を利用できれば、ユーザーはNeovimを別途インストールしたり、バックエンドとなるNeovimの実行環境を用意したりせず、拡張機能だけで利用できる可能性があります。拡張機能とNeovimのバージョンを揃えやすくなり、OSごとのバイナリ配布も不要になります。
Firenvimは、WebページのtextareaなどをNeovimで編集できるようにするブラウザ拡張です。現在はFirenvimをローカルのNeovimプラグインとしてインストールし、ブラウザのNative Messagingを通じてNeovimを起動します。
NeovimをWasmとしてブラウザ内で起動できれば、ローカルのNeovimやNative Messaging用のホストを用意せず、ブラウザ拡張だけで同様の仕組みを実現できる可能性があります。Firenvimのような専用UIだけでなく、Webページ上の一般的なテキスト入力をNeovim経由で操作する仕組みにも応用できると思います。
ファイルシステム、設定ファイル、プラグイン、ブラウザの権限制約など、実用化にはまだ解決すべき点があります。興味があれば、ぜひnvim-wasmの実装を参考に取り組んでみてはいかがでしょうか。
おわりに
Neovimをブラウザで動かすには、CをWasmへコンパイルするだけでは足りませんでした。クロスコンパイル中のコード生成、libuvとWASIの差、仮想ファイルシステム、入力待ち、Msgpack-RPC、UI描画まで接続して、初めてブラウザから操作できます。
nvim-wasmでは、最小構成のDOM、Monaco Editor、ターミナル、Asyncifyという異なる方法で、その接続部分を試せるようにしました。
今後、Neovim本体のWasm対応が進む際にも、この実装で得られた知見が役立てばと思います。
参考資料
- MuNeNiCK/nvim-wasm
- Neovim: tracking: WASM as Nvim build target
- Neovim: GSoC 2026 WASM/Emscripten tracking issue
- Neovim PR #40008: add wasm/emscripten build support
- Neovim PR #40772: add browser support for Neovim
- vscode-neovim
- vscode-neovim: ship Nvim built as WebAssembly
- Firenvim
- WASI SDK
- Binaryen Asyncify
- Neovim UI events