Posted at
ElixirDay 17

Vim上でElixirの補完を実装した時の苦労話

More than 3 years have passed since last update.

以前 Qiita に Elixir初心者がVimで開発環境を整えてみた という記事を書き、その流れで vivi.vim という Vim プラグインを作りました。

Elixir 開発環境構築をサポートする Vim プラグインです。

それから残念ながら Elixir 初心者を脱せてはいないのですが、vivi.vim の ver 0.1.0 にて

Elixir の補完機能が実装できたので、Elixir 視点での実装における苦労話などまとめてみます。


どんなことができるようになったか

vivi.gif

このようにモジュール内の関数が補完できるようになり、ついでにドキュメントも参照できるようにてあります。


簡単な原理の説明

Vim Advent Calendar ではないのであくまで簡単な説明ですが、要は裏で iex -S mix を起動して、repl と通信して補完候補やドキュメントを取得しています。

なお VimConf 2015@ujm さんの neoclojure.vim の発表を聞かなければ、この補完機能は実現できませんでした。

@ujmさんには感謝しかありません。


補完候補の取得方法

さて、Elixir の話に戻りまして、補完候補をどうやって取得するかを説明します。


__info__/1 を使う方法

すべての Module には __info__/1 という関数が定義されています。

これを使うと public な関数名と arity のペアが取得できます。

iex> Atom.__info__(:functions)

[to_char_list: 1, to_string: 1]

最低限の補完であればこれで問題ないですが、できれば引数名も補完時に表示できると嬉しいです。

なので __info__/1 は今回不採用としました。


Code.get_docs/2 を使う方法

何か方法はないのかと探していたとき、ふと IEx 上の h/1 で表示されるドキュメントに

引数名が表示されていることを思い出して、IExのソース を読んでいて発見しました。

Code.get_docs/2 の存在を。

Code.get_docs/2 はその名のとおり、docstring を返しますが同時に引数の情報も返してくれます。これは素敵。

iex> Code.get_docs(Atom, :docs)

[{{:to_char_list, 1}, 22, :def, [{:atom, [], nil}],
"Converts an atom to a char list.\n\nInlined by the compiler.\n"},
{{:to_string, 1}, 12, :def, [{:atom, [], nil}],
"Converts an atom to string.\n\nInlined by the compiler.\n"}]

h/1 ではこの情報をいい感じに文字列に変換して表示しているので、それと同じことをしてあげれば補完候補を作ることができます。

具体的にどのように文字列変換しているかは以下のソースをご参照ください。

https://github.com/liquidz/vivi.vim/blob/master/elixir/lib/vivi.ex

何やら複雑なことをしていますが、これは高階関数の引数情報がちょっと複雑な形式で返ってくるのを文字列に変換するためです。

iex> Code.get_docs(Enum, :docs) |> List.first

{{:all?, 2}, 222, :def,
[{:collection, [], nil},
{:\\, [],
[{:fun, [], nil},
{:fn, [line: 222],
[{:->, [line: 222],
[[{:x, [line: 222], nil}], {:x, [line: 222], nil}]}]}]}],
"Invokes the given `fun` for each item in the `collection` and returns `false`\nif at least one invocation returns `false`. Otherwise returns `true`.\n\n## Examples\n\n iex> Enum.all?([2, 4, 6], fn(x) -> rem(x, 2) == 0 end)\n true\n\n iex> Enum.all?([2, 3, 4], fn(x) -> rem(x, 2) == 0 end)\n false\n\nIf no function is given, it defaults to checking if\nall items in the collection evaluate to `true`.\n\n iex> Enum.all?([1, 2, 3])\n true\n\n iex> Enum.all?([1, nil, 3])\n false\n\n"}

iex> h(Enum.all?/2)

def all?(collection, fun \\ fn x -> x end)

これでモジュールの関数リストとそれぞれの引数情報が取得できるようになったので、あとはそれをもとに補完するための処理を Vim なり Emacs なりで書いてあげれば補完機能が実現できます。

めでたしめでたし!

。。。そう思っていた時期が私にもありました。


補完候補の更新

Atom, Enum といった Elixir のコアモジュールはコーディング中に関数名や引数名が変わることはありませんが、今まさにコーディング中のモジュールは関数が増えることも引数名が変わることもしょっちゅうです。

それらのモジュールについても Code.get_docs/2 で情報は取得できるので補完はできますが、変更された場合には補完候補も同様に変更されるのが自然です。


r/1 でのモジュールリロード

真っ先に思いつくのが IEx で使える r/1 です。

これでリコンパイル&リロードをしてあげれば、モジュールの変更が反映され・・ない!

最初は r/1 の仕様なのかなと思っていたのですが、改めて確認すると IEx 上では __info__/1 の結果も Code.get_docs/2 の結果も両方反映されるところ、Vim プラグインと IEx との連携では何故か反映されません。。

iex> Foo.__info__(:functions)

[hello: 1]
iex> r(Foo)
lib/foo.ex:3: warning: redefining module Foo
{:reloaded, Foo, [Foo]}

iex> Foo.__info__(:functions)
[helloworld: 2]

iex> Code.get_docs(Foo, :docs)

[{{:hello, 1}, 4, :def, [{:a, [], nil}], nil}]
iex> r(Foo)
lib/foo.ex:3: warning: redefining module Foo
{:reloaded, Foo, [Foo]}

iex> Code.get_docs(Foo, :docs)
[{{:helloworld, 2}, 4, :def, [{:a, [], nil}, {:b, [], nil}], nil}]

IEx との連携処理をしているライブラリの問題なのか、そもそも使い方が悪いのかまだわかっていませんがこれは困りました。


神降臨

どうしたものかと解決策を探していたのですが、elixir-lang-talk フォーラムに神が降臨してました。

José 様です。

Reloading an entire project in IEx


Here is some code you can try out. Put this in a .iex.exs file inside your project root:

defmodule R do

def reload! do
Mix.Task.reenable "compile.elixir"
Application.stop(Mix.Project.config[:app])
Mix.Task.run "compile.elixir"
Application.start(Mix.Project.config[:app], :permanent)
end
end

Now you can do: R.reload!

Others are welcome to try it. We could even include it in iex helpers by default if it is working fine for multiple users (notice it won't reload your configuration though).


上記のコードを ~/.iex.exs に書いてあげて、IEx 上で R.reload! すればプロジェクト全体のリロードができるよという回答になります。

vivi.vim では現状これで Code.get_docs/2 の情報への反映が確認できたので、vivi#iex#reload() という関数でリロードできるようにしています。

ただプロジェクト全体をリロードするとのことなので、大きいプロジェクトではリロードの影響で例えば動作が遅くなるなどあるかもしれません。

その点は r/1 でのリロード方法と併せて、今後の要確認事項かなと思っています。


最後に

補完機能を実現するという目的のおかげで、普段はあまり使わない関数を知ったり、IEx helper のコードを読んだりとなかなか面白い経験ができ、Elixir レベルもほんのちょっとは上がったのかなと思います。

この知識が直接、Elixir でのサービスやライブラリ開発に役立つかというと疑問しか残りませんが、どこかで役立つと願いつつまとめてみました。

Vim で Elixir を書いているかたは是非、vivi.vim を試してみていただき感想をもらえると大変ありがたいです!