FennelでNeovimプラグインを書こう
この記事はVimアドベントカレンダーシリーズ2 19日目の記事になります。
Fennelとは
Fennelとは、LuaにトランスパイルされるLisp方言です。
LuaにトランスパイルできるということはNeovimで実行ができるということ、つまりNeovimプラグインが書けると考えるのは全人類共通かと思います。[1]
というよりNeovimプラグインを漁っているときにこの言語を見つけました。
Fennelで書かれたプラグインが存在するという事は、Neovimプラグインを作成しやすくするプラグインがありそうですね。それがこちらです。
この記事ではこのnfnlを使ってFennelでNeovimプラグインを書く方法を解説します。
ちなみに、FennelにはLSPとTreesitter向けperserが用意されているため、NeovimでMasonとTreesitterを使っている方は秒速で開発環境を用意できるはずです。
もしFennel構文が気になる方はこちらのチュートリアルが分かりやすいのでオススメです。
Fennelのセットアップ
まず手始めにFennelのセットアップをします。FennelはLua 5.1, 5.2, 5.3, 5.4とLuaJITを必要とするので、事前にこれらをインストールします。
次にFennelをインストールします。Fennelは単一バイナリファイルを提供しているため、これをダウンロードしてPATHを通すのが手っ取り早いです。
nfnlのセットアップ
nfnlはNeovimプラグインですので、お好きなプラグインマネージャーでインストールしてください。僕はdpp.vimでインストールしました。
手前味噌ですがNeovimでdpp.vimをインストールする記事も書いているので、気になる方はぜひ。
プラグイン作りの下準備
今回は簡単な四則演算をするプラグインを作ってみます。
次にプラグイン用のディレクトリを作成します。ここではプラグイン名をcalc.nvim
とします。
mkdir ./calc.nvim
次にnfnlの設定ファイルである.nfnl.fnl
を作成します。
中は以下のように記述してください。
{:source-file-patterns ["fnl/**/*.fnl"]}
次にプラグインの本体となる./fnl
ディレクトリを作成します。
mkdir -p ./fnl/calc
calc
ディレクトリの中にinit.fnl
ファイルを作成します。
touch ./fnl/calc/init.fnl
最後に、Neovimがプラグインを読み込めるようにruntimepathを設定します。ここではLuaでの設定方法を紹介します。
vim.opt.runtimepath:append(vim.fn.expand("~/path/to/your/plugin/dir/calc.nvim"))
これで下準備は完了です。
Fennelでプラグインを書く
まずは手始めにHello worldしてみます。ここからはLispのコードが出てきますが、以下のルールを頭に入れておけば大丈夫です。
- リテラル、関数などは全て
()
で囲う - 関数は
(関数名 引数...)
で呼び出す - 関数の定義は
(fn 関数名 [引数])
と書く - 関数のexportは
{: 関数名}
と書く
それではさっそく書いていきます。
(fn setup []
(print "Hello")
)
{: setup}
コードが書けたら:w
してプロジェクトディレクトリをls
してみてください。
わーお。いつの間にか./lua/init.lua
というファイルが作成されています。
.
├── fnl
│ └── calc
│ └── init.fnl
└── lua
└── calc
└── init.lua
これはnfnlの機能で、.nfnl.fnl
で指定されたファイルが保存された時は自動的にトランスパイルして結果をNeovimが読み込める位置に配置してくれます。
この機能があるおかげで開発体験がとても良くなっています。ありがたい...
トランスパイル結果を見てみるとこんな感じになっていると思います。
-- [nfnl] Compiled from fnl/calc/init.fnl by https://github.com/Olical/nfnl, do not edit.
local function setup()
print("Hello")
end
return {setup = setup}
一般的なLuaプラグインみたいにsetup()
関数が定義されています。
それではさっそく呼び出してみましょう。
lua require("calc").setup()
Hello
と表示されたら成功です。
Neovimの機能を使ってみる
いくらLuaにトランスパイル出来てもNeovimの機能が使えないと意味がありません。
FennelはLuaにトランスパイルされる言語なのでLuaとの高い互換性を備えています。なので、Fennelの関数を呼ぶ感覚でLuaの関数を呼び出せます。
手始めにvim.fn.expand()
関数を呼び出してみましょう。"~/ghq"
の箇所は任意のpathで大丈夫です。
(fn setup []
(print "Hello")
+ (print (vim.fn.expand "~/ghq"))
)
{: setup}
もう一度実行してみるとHello
とvim.fn.expand()
した結果が表示されるはずです。
このように、Fennelを使うとLispとLuaのいいとこ取りをしながらコードが書けます。
プラグインの機能を作っていく
それでは今度はプラグインの機能を作り込んでいきましょう。とりあえず今回は空白区切りの四則演算のみ実装してみます。
先に全体のコードを貼っておきます。
全体のコード
(fn calc [opts]
(let [
args (vim.split opts.args " ")
a (. args 1)
expr (. args 2)
b (. args 3)
]
(print (if (= expr "+")
(+ a b)
(= expr "-")
(- a b)
(= expr "*")
(* a b)
(= expr "/")
(/ a b)
)
))
)
(fn setup []
(let [opt {}] opt
(tset opt "nargs" 1)
opt
(vim.api.nvim_create_user_command "Calc" calc opt)
":ok"
)
)
(let [M {}]
(tset M "setup" setup)
M
)
ここからはそれぞれ重要な箇所を解説していきます。
(fn calc [opts]
(let [
args (vim.split opts.args " ")
a (. args 1)
expr (. args 2)
b (. args 3)
]
(print (if (= expr "+")
(+ a b)
(= expr "-")
(- a b)
(= expr "*")
(* a b)
(= expr "/")
(/ a b)
)
))
)
四則演算を行うメイン部分です。
vim.api.nvim_create_user_command
のcallbackとして指定するため、引数は一つにします。
与えられた引数はopts.args
に文字列として格納されているため、vim.split
関数を用いてsplitします。[2]
戻り値はtableなので.
関数を使って要素を取り出します。
これらの処理はlet
関数内で行われていて、この関数内ではlet
関数の引数に指定した値(a, expr, b)を変数として使えます。
その後if
関数を使ってexpr
がそれぞれの演算子(+, -, *, /)のどれかに等しいか判定し、もしどれかに当てはまったらその計算結果を返します。
if
関数はprint
関数に囲まれているため、結果がNeovimのコマンドラインに表示されます。
(fn setup []
(let [opt {}]
(tset opt "nargs" 1)
(vim.api.nvim_create_user_command "Calc" calc opt)
":ok"
)
)
先ほどのcalc
関数をユーザーコマンドとして登録する処理です。
nvim_create_user_command
関数は
- コマンド名
- Vim script or Luaのcallback関数
- オプション
の3つの引数を受け付けます。
このうち最後のオプションはtableで指定するため、let
関数でスコープを作り、その中でオプションとなるopt
変数を作成、nargs
キーに1
を指定しています。
nvim_create_user_command
関数はlet
関数内に書かれているため、そのままopt
を引数として指定できます。
最後の":ok"ですが、これがないとreturn nvim_create_user_command()
というLuaスクリプトが出力されてしまい、意図した挙動をしなくなるため追加してあります。
何らかの値なら何でも良いのですが、僕はElixirっぽく":ok"
と書きました。
(let [M {}]
(tset M "setup" setup)
M
)
最後のこの処理ですが、先ほどのopt
みたいにlet
関数内でテーブルを作成し、keyとvalueをテーブルにセットしてそのテーブルをreturn
しています。
このlet
関数はトップレベルにあるため、これはスクリプトレベルでのreturn
となります。
Luaにトランスパイルされた結果は以下のようになります。
local M = {}
M["setup"] = setup
return M
見慣れたモジュールのexport処理になっています。
余談
Fennelという言語は前々から気になっていたのですが、なんとZennに一つも記事がないためマイナー言語好きとして記事を書いてみました。
vim-jpのSlackでもちょくちょく話題に挙がったりするのでさらなる知見の蓄積に期待していきたいです。
Discussion