neovimで非同期プラグインの作成

## vimで非同期処理がしたい

- vim自体で別threadたてれない
- vimproc + autocmdで頑張る
    - コールバックの管理辛い
- job/channel
    - 知らない子ですね（良さそう）
- remote plugin
    - 今日はこの話

---

## Remote Plugin in neovim

- neovimの新機能
- プラグインを別プロセスで起動
    - プラグインのプロセスが死んでもneovimは無事！
- msgpack-rpcで相互通信（非同期）
- 原理的にどんな言語でも書ける！
    - `if_python`とか`if_lua`とかとおさらば！！

---

## msgpack-RPC?

- msgpack ~= JSONのバイナリ版
- RPC = remote procedure call
    - 関数が外部に公開されている`int func(int)`
    - クライアントは関数呼び出しっぽく呼べる`a = client.call("func", 2);`
    - サーバー・クライアントが別言語でもmsgpackに変換さえできればOK
    - 最近はgRPCの方が流行りかも？
- neovimとpluginの両方が双方向に送受信する

---

## neovim server

- "The Nvim C API is automatically exposed to the RPC API by the build system"
    - なんでもできる(/・ω・)/
- 試しに呼んでみよう
    - `NVIM_LISTEN_ADDRESS=127.0.0.1:6666 nvim`で起動
    - （python clientがUNIX socketに対応してない）
    - neovim自体もmsgpack-RPCクライアントになる -> [別記事](http://qiita.com/termoshtt/items/1649180e7610a5f717f1)

```py
import mprpc
c = mprpc.RPCClient("localhost", 6666)
_, info = c.call("vim_get_api_info")
for f in info["functions"]:
    print(f["name"], f["parameters"], f["return_type"])
```

---

呼べる関数の一覧が取得できた：

```
buffer_line_count (('Buffer', 'buffer'),) Integer
buffer_get_line (('Buffer', 'buffer'), ('Integer', 'index')) String
buffer_set_line (('Buffer', 'buffer'), ('Integer', 'index'), ('String', 'line')) void
buffer_del_line (('Buffer', 'buffer'), ('Integer', 'index')) void
buffer_get_line_slice (('Buffer', 'buffer'), ('Integer', 'start'), ('Integer', 'end'), ('Boolean', 'include_start'), ('Boolean', 'include_end')) ArrayOf(String)
buffer_get_lines (('Buffer', 'buffer'), ('Integer', 'start'), ('Integer', 'end'), ('Boolean', 'strict_indexing')) ArrayOf(String)
buffer_set_line_slice (('Buffer', 'buffer'), ('Integer', 'start'), ('Integer', 'end'), ('Boolean', 'include_start'), ('Boolean', 'include_end'), ('ArrayOf(String)', 'replacement')) void
buffer_set_lines (('Buffer', 'buffer'), ('Integer', 'start'), ('Integer', 'end'), ('Boolean', 'strict_indexing'), ('ArrayOf(String)', 'replacement')) void
buffer_get_var (('Buffer', 'buffer'), ('String', 'name')) Object
buffer_set_var (('Buffer', 'buffer'), ('String', 'name'), ('Object', 'value')) Object
buffer_del_var (('Buffer', 'buffer'), ('String', 'name')) Object
buffer_get_option (('Buffer', 'buffer'), ('String', 'name')) Object
buffer_set_option (('Buffer', 'buffer'), ('String', 'name'), ('Object', 'value')) void
buffer_get_number (('Buffer', 'buffer'),) Integer
buffer_get_name (('Buffer', 'buffer'),) String
buffer_set_name (('Buffer', 'buffer'), ('String', 'name')) void
buffer_is_valid (('Buffer', 'buffer'),) Boolean
buffer_insert (('Buffer', 'buffer'), ('Integer', 'lnum'), ('ArrayOf(String)', 'lines')) void
buffer_get_mark (('Buffer', 'buffer'), ('String', 'name')) ArrayOf(Integer, 2)
buffer_add_highlight (('Buffer', 'buffer'), ('Integer', 'src_id'), ('String', 'hl_group'), ('Integer', 'line'), ('Integer', 'col_start'), ('Integer', 'col_end')) Integer
buffer_clear_highlight (('Buffer', 'buffer'), ('Integer', 'src_id'), ('Integer', 'line_start'), ('Integer', 'line_end')) void
tabpage_get_windows (('Tabpage', 'tabpage'),) ArrayOf(Window)
tabpage_get_var (('Tabpage', 'tabpage'), ('String', 'name')) Object
tabpage_set_var (('Tabpage', 'tabpage'), ('String', 'name'), ('Object', 'value')) Object
tabpage_del_var (('Tabpage', 'tabpage'), ('String', 'name')) Object
tabpage_get_window (('Tabpage', 'tabpage'),) Window
tabpage_is_valid (('Tabpage', 'tabpage'),) Boolean
vim_command (('String', 'str'),) void
vim_feedkeys (('String', 'keys'), ('String', 'mode'), ('Boolean', 'escape_csi')) void
vim_input (('String', 'keys'),) Integer
vim_replace_termcodes (('String', 'str'), ('Boolean', 'from_part'), ('Boolean', 'do_lt'), ('Boolean', 'special')) String
vim_command_output (('String', 'str'),) String
vim_eval (('String', 'str'),) Object
vim_call_function (('String', 'fname'), ('Array', 'args')) Object
vim_strwidth (('String', 'str'),) Integer
vim_list_runtime_paths () ArrayOf(String)
vim_change_directory (('String', 'dir'),) void
vim_get_current_line () String
vim_set_current_line (('String', 'line'),) void
vim_del_current_line () void
vim_get_var (('String', 'name'),) Object
vim_set_var (('String', 'name'), ('Object', 'value')) Object
vim_del_var (('String', 'name'),) Object
vim_get_vvar (('String', 'name'),) Object
vim_get_option (('String', 'name'),) Object
vim_set_option (('String', 'name'), ('Object', 'value')) void
vim_out_write (('String', 'str'),) void
vim_err_write (('String', 'str'),) void
vim_report_error (('String', 'str'),) void
vim_get_buffers () ArrayOf(Buffer)
vim_get_current_buffer () Buffer
vim_set_current_buffer (('Buffer', 'buffer'),) void
vim_get_windows () ArrayOf(Window)
vim_get_current_window () Window
vim_set_current_window (('Window', 'window'),) void
vim_get_tabpages () ArrayOf(Tabpage)
vim_get_current_tabpage () Tabpage
vim_set_current_tabpage (('Tabpage', 'tabpage'),) void
vim_subscribe (('String', 'event'),) void
vim_unsubscribe (('String', 'event'),) void
vim_name_to_color (('String', 'name'),) Integer
vim_get_color_map () Dictionary
vim_get_api_info () Array
window_get_buffer (('Window', 'window'),) Buffer
window_get_cursor (('Window', 'window'),) ArrayOf(Integer, 2)
window_set_cursor (('Window', 'window'), ('ArrayOf(Integer, 2)', 'pos')) void
window_get_height (('Window', 'window'),) Integer
window_set_height (('Window', 'window'), ('Integer', 'height')) void
window_get_width (('Window', 'window'),) Integer
window_set_width (('Window', 'window'), ('Integer', 'width')) void
window_get_var (('Window', 'window'), ('String', 'name')) Object
window_set_var (('Window', 'window'), ('String', 'name'), ('Object', 'value')) Object
window_del_var (('Window', 'window'), ('String', 'name')) Object
window_get_option (('Window', 'window'), ('String', 'name')) Object
window_set_option (('Window', 'window'), ('String', 'name'), ('Object', 'value')) void
window_get_position (('Window', 'window'),) ArrayOf(Integer, 2)
window_get_tabpage (('Window', 'window'),) Tabpage
window_is_valid (('Window', 'window'),) Boolean
```

---

## msgpack-RPCわからないと書けないの？

そんなことはない：各言語のSDKが開発されている

- Python : neovim/python-client
- Node.js : neovim/node-client
- 他にもThrid party製のものがたくさん
    - https://github.com/neovim/neovim/wiki/Related-projects#api-clients

---

## neovim/python-clientでプラグインを作る

ここからが本題。[toggl.nvim](https://github.com/termoshtt/toggl.nvim)を作ってみて（途中だけど）調べたことをまとめる。

- `$runtimepath/rplugin/python3/`にファイルを置く
    - モジュールを置く場合空ファイルを置かないとうまく動かない

```
rplugin/python3/toggl/...
                toggl.py  # 空ファイル
```

- document少なめ
    - deoplete.nvimを参考に
    - python-clientのソース見る（割と見やすい）

- RPC関係は全部neovim/python-clientがやってくれる|ω・)

---

## python-clientの使い方

[toggl.nvim](http://github.com/termoshtt/toggl.nvim)より抜粋

```py3
@neovim.plugin
class Toggl(object):

    def __init__(self, nvim):
        self.nvim = nvim
        self.api_token = nvim.eval("g:toggl_api_token")
        self.api = TogglAPI(self.api_token)

    @neovim.autocmd("VimEnter")
    def update(self):
        try:
            self.wid = self.api.workspaces()[0]["id"]
            self.projects = self.get_projects([])
        except ConnectionError:
            self.echo("No network, toggl.nvim is disabled.")

    def echo(self, msg):
        self.nvim.command("echo '{}'".format(msg))

    @neovim.function("TogglAPIToken", sync=True)
    def api_token(self, args):
        return self.api_token

    @neovim.function("TogglGetCurrent", sync=True)
    def get_current(self, args):
        return self.api.time_entries.current()
```

---

デコレータ付きでクラス＋関数を定義するだけでvimプラグインが書ける

- neovim.pluginをclassにつけるとnvimインスタンスがもらえる
- neovim.autocmdでautocmdを発行できる
- neovim.commandでvimのコマンドつくれる（引数は自動的にPythonの変数に変換される）
- neovim.functionでvimの関数つくれる
- プラグイン側は別プロセスなので自由に状態を持てる
- 同期な関数(sync=True)と非同期な関数(sync=False, default)の両方を同じようにかける
- Pythonのライブラリ使える(requests)とか

Pythonかけるけどvim scriptはちょっと…な人にオススメ！

---

## 最後に

- Qiita slide簡単で良い
- osaka.vim楽しい