Neovim は Vim とは異なり、プラグインを書く際に言語ごとのインターフェイスというものは存在せず、MessagePack-RPC で Neovim とは独立して動くプラグインと通信をして色々と操作するというモデルのようです。
ということで、Haskell で Neovim のプラグインを書いてみました。
TL;DR
GitHub に動作するサンプルコードを置きました。ビルドして実行すれば以下のように動きます。
説明など
nvim-hs という Neovim のプラグインプロバイダーを利用して実装しました。このパッケージにも丁寧な説明が書かれているのですが、私は Neovim も Vim もプラグインを書いたことがないので、勝手がわからず結構つまづきました。
ひとつ目はファイルの構成です。
.
├── app
│ └── Main.hs
└── src
├── Hello
│ └── Plugin.hs
└── Hello.hs
ソースはこんな感じになっています。nvim-hs の説明にもしっかり書かれていたのですが、Hello.hs と Plugin.hs は Hello.hs で Template Haskell を使用している都合上、まとめることができないようです。なので、実装を Plugin.hs に書いて、設定などを Hello.hs に書くようにします。
ふたつ目は Neovim から呼び出されるエントリポイントになるシェルスクリプトです。
シェルスクリプトは以下のようになり、チェックなどを省くとビルドしてできたバイナリを stack 経由で呼び出しているだけです。
#!/bin/bash
plugin_name=neovim-plugin-hello-haskell
plugin_dir="$(cd $(dirname $0) && pwd)"
pushd $plugin_dir > /dev/null
if [ -d "$plugin_dir/.stack-work" ]; then
stack exec $plugin_name-exe -- "$@"
rc=0
else
echo "No development directories found. Have you built the project?"
rc=2
fi
popd > /dev/null
exit $rc
nvim-hs のオリジナルのサンプルは、stack 経由で nvim-hs のバイナリを呼び出していたため、いったいどういう仕組みで実装したコードが実行されるのかがわからず、ここでかなり遠回りしました。
dein.vim で管理できるようにする
お作法として GitHub のリポジトリを以下のようにしておく必要があるようです。
.
├── autoloaded
│ └── neovim-plugin-hello-haskell.vim
└── plugin
└── neovim-plugin-hello-haskell.vim
autoloaded は必要になったタイミングで読み込まれる処理を、plugin
は Neovim 起動時に読み込まれる処理に分けるのが良いようです。今回のサンプルは関数ひとつなので、plugin
に置きました。
Vim Script の名前はなんでも大丈夫なようです。おそらく runtime!
で読み込んでいるのでしょう。
この Vim Script は頑張って Haskell で書こうとせずに VimL で書くほうが楽だと思います。ほとんどボイラープレートなので、VimL がよくわからなくても一度作ってしまえば以降は困りません。
if !has('nvim')
finish
endif
if exists("g:loaded_neovim_plugin_hello_haskell") " ここと
finish
endif
let g:loaded_neovim_plugin_hello_haskell = 1 " ここの変数名だけ変える
let s:save_cpo = &cpo
set cpo&vim
let s:script_dir = expand('<sfile>:p:h')
call rpcrequest(rpcstart(s:script_dir . '.sh'), "PingNvimhs")
let &cpo = s:save_cpo
unlet s:save_cpo
上記スクリプトを少しだけ解説します。
if has('nvim')
は、Neovim だけで動作させたい場合のガードです。Haskell のプラグインは Neovim でしか動作しないので付けておいたほうが親切です。
if exixts(...)
から let ...
は 2 回以上実行しないためのガードです。 g:loaded_
という変数名で開始するのが慣習のようです。変数にプラグイン名を含めるとバッティングしなそうです。
次の let s:save_cpo = &cpo
と set cpo&vim
はプラグインを読み込む際にユーザの設定を一時的に無効にするおまじないです。
その次の 2 行がプラグインの処理です。
最後の let &cpo = s:save_cpo
と unlet s:save_cpo
は、一時的に無効にしたユーザの設定をもとに戻すおまじないです。
今後の課題
- ステートフルなプラグインの作成
プラグインマネージャで管理するためのお作法