🦊

FennelでNeovimプラグインを書こう

2023/12/19に公開

この記事はVimアドベントカレンダーシリーズ2 19日目の記事になります。

https://qiita.com/advent-calendar/2023/vim

Fennelとは

Fennelとは、LuaにトランスパイルされるLisp方言です。

https://fennel-lang.org

LuaにトランスパイルできるということはNeovimで実行ができるということ、つまりNeovimプラグインが書けると考えるのは全人類共通かと思います。[1]
というよりNeovimプラグインを漁っているときにこの言語を見つけました。

https://github.com/ggandor/leap.nvim

Fennelで書かれたプラグインが存在するという事は、Neovimプラグインを作成しやすくするプラグインがありそうですね。それがこちらです。

https://github.com/Olical/nfnl

この記事ではこのnfnlを使ってFennelでNeovimプラグインを書く方法を解説します。

ちなみに、FennelにはLSPとTreesitter向けperserが用意されているため、NeovimでMasonとTreesitterを使っている方は秒速で開発環境を用意できるはずです。

もしFennel構文が気になる方はこちらのチュートリアルが分かりやすいのでオススメです。

https://fennel-lang.org/tutorial

Fennelのセットアップ

まず手始めにFennelのセットアップをします。FennelはLua 5.1, 5.2, 5.3, 5.4とLuaJITを必要とするので、事前にこれらをインストールします。
次にFennelをインストールします。Fennelは単一バイナリファイルを提供しているため、これをダウンロードしてPATHを通すのが手っ取り早いです。

nfnlのセットアップ

nfnlはNeovimプラグインですので、お好きなプラグインマネージャーでインストールしてください。僕はdpp.vimでインストールしました。

手前味噌ですがNeovimでdpp.vimをインストールする記事も書いているので、気になる方はぜひ。

https://zenn.dev/comamoca/articles/howto-setup-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での設定方法を紹介します。

init.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}

もう一度実行してみるとHellovim.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でもちょくちょく話題に挙がったりするのでさらなる知見の蓄積に期待していきたいです。

脚注
  1. 元ネタ ↩︎

  2. Luaにはsplit関数が実装されていないのでとてもありがたい... ↩︎

GitHubで編集を提案

Discussion