Haskell
neovim

Neovim のプラグインを Haskell で書いてみる

More than 1 year has passed since last update.

Neovim は Vim とは異なり、プラグインを書く際に言語ごとのインターフェイスというものは存在せず、MessagePack-RPC で Neovim とは独立して動くプラグインと通信をして色々と操作するというモデルのようです。

ということで、Haskell で Neovim のプラグインを書いてみました。

TL;DR

GitHub に動作するサンプルコードを置きました。ビルドして実行すれば以下のように動きます。

screenshot-01.png

screenshot-02.png

説明など

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 = &cposet cpo&vim はプラグインを読み込む際にユーザの設定を一時的に無効にするおまじないです。

その次の 2 行がプラグインの処理です。

最後の let &cpo = s:save_cpounlet s:save_cpo は、一時的に無効にしたユーザの設定をもとに戻すおまじないです。

今後の課題

  • ステートフルなプラグインの作成
  • プラグインマネージャで管理するためのお作法