[[Neovimプラグイン]]として[[🦉ghostwriter.nvim]]を作ったが、非同期処理が結局実装できないままでいた。文献を漁って[[🦉ghostwriter.nvim]]で試した限りでは上手くいかなかったため、1から勉強用のプロジェクトを作成し、丁寧に理解していく。
## 参考記事
<div class="link-card">
<div class="link-card-header">
<img src="https://cdn.qiita.com/assets/favicons/public/production-c620d3e403342b1022967ba5e3db1aaa.ico" class="link-card-site-icon"/>
<span class="link-card-site-name">Qiita</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<p class="link-card-title">plenary.nvim による非同期処理 - Qiita</p>
<p class="link-card-description">1. plenary.nvim とは?Neovim 界で一番メジャーなプラグインとは何でしょう? 異論はあるかも知れませんが、多くの人は telescope.nvim を挙げると思います。ファジー…</p>
</div>
<img src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fcdn.qiita.com%2Fassets%2Fpublic%2Fadvent-calendar-ogp-background-7940cd1c8db80a7ec40711d90f43539e.jpg?ixlib=rb-4.0.0&w=1200&mark64=aHR0cHM6Ly9xaWl0YS11c2VyLWNvbnRlbnRzLmltZ2l4Lm5ldC9-dGV4dD9peGxpYj1yYi00LjAuMCZ3PTk3MiZoPTM3OCZ0eHQ9cGxlbmFyeS5udmltJTIwJUUzJTgxJUFCJUUzJTgyJTg4JUUzJTgyJThCJUU5JTlEJTlFJUU1JTkwJThDJUU2JTlDJTlGJUU1JTg3JUE2JUU3JTkwJTg2JnR4dC1hbGlnbj1sZWZ0JTJDdG9wJnR4dC1jb2xvcj0lMjMzQTNDM0MmdHh0LWZvbnQ9SGlyYWdpbm8lMjBTYW5zJTIwVzYmdHh0LXNpemU9NTYmcz05NDY1MzFhMzVmZmE3ODU0NjMzYjFmMTYxZTQ1MGY1Zg&mark-x=120&mark-y=96&blend64=aHR0cHM6Ly9xaWl0YS11c2VyLWNvbnRlbnRzLmltZ2l4Lm5ldC9-dGV4dD9peGxpYj1yYi00LjAuMCZoPTc2Jnc9OTcyJnR4dD0lNDBkZWxwaGludXMmdHh0LWNvbG9yPSUyMzNBM0MzQyZ0eHQtZm9udD1IaXJhZ2lubyUyMFNhbnMlMjBXNiZ0eHQtc2l6ZT0zNiZ0eHQtYWxpZ249bGVmdCUyQ3RvcCZzPTgyNjRjMDE1MWQxNWY3M2YxNmZiOTY5OGFjNWRiNGQy&blend-x=120&blend-y=445&blend-mode=normal&txt64=aW4g5qCq5byP5Lya56S-44OH44Kj44O844O744Ko44OM44O744Ko44O8&txt-width=972&txt-clip=end%2Cellipsis&txt-color=%233A3C3C&txt-font=Hiragino%20Sans%20W6&txt-size=36&txt-x=134&txt-y=546&s=c9898d1b5c44c0cd2cf4d5f658ba1ee3" class="link-card-image" />
</div>
<a href="https://qiita.com/delphinus/items/2d60bd2f00834c7709e6"></a>
</div>
## コードベース
1秒かかるリクエストをするコード。
```lua
local curl = require("plenary.curl")
local function run_test()
vim.notify("loading...")
curl.get("https://reqres.in/api/users?delay=1")
vim.notify("hoge")
end
vim.api.nvim_create_user_command("Hoge", function()
run_test()
end, { nargs = 0 })
```
> [!hint]
> エンドポイントは[[Reqres]]の遅延時間(`delay`)を指定できるAPIを使っている。
最終的に非同期対応したあとの期待値としては以下。
- `loading...`が表示されること
- sleep中も[[Neovim]]の操作ができること
## コールバックを利用する
[[plenary.nvim]]のコードを確認すると、第2引数 `opts` に `callback` が指定されている場合は非同期で処理されるようになっていた。のでそのようにする。
```lua
local curl = require("plenary.curl")
local function run_test()
vim.notify("loading...")
curl.get("https://reqres.in/api/users?delay=1", {
callback = function()
vim.notify("hoge")
end,
})
end
vim.api.nvim_create_user_command("Hoge", function()
run_test()
end, { nargs = 0 })
```
これで動作は期待通りになった。ただ、このままではcallback地獄の餌食となる。。
## asyncで同期的な記述を
`async.wrap`で関数をwrapすれば非同期関数になりそうなのでやってみた。
```lua
local async = require("plenary.async")
local curl = require("plenary.curl")
-- curl.getとのIF差分を吸収するための薄いラッパー
-- curl.getは第1引数がtableの場合は、callbackキーにcallbackを設定する実装になっている
local async_get = async.wrap(function(opts, callback)
opts.callback = callback
curl.get(opts)
end, 2)
-- query: { delay = number }
local function get_users(query)
return async_get({
url = "https://reqres.in/api/users",
query = query,
})
end
local function run_test()
vim.notify("before request")
local r = get_users({ delay = 1 })
vim.notify("total: " .. vim.json.decode(r.body).total)
end
vim.api.nvim_create_user_command("Hoge", function()
async.void(function()
vim.notify("command start")
run_test()
vim.notify("command end")
end)()
end, { nargs = 0 })
```
> [!attention]
> `async.wrap`の第2引数で指定する値は、第1引数で指定した関数の引数の数だが、**callbackの引数も数に含まれる** ので注意。それを抜いた(-1した)値にするとブロッキングされてしまう。
ポイントと感じた点は2つ。
- 最後の引数が`callback`になる関数IFが存在しなければ用意し、その関数実装では非同期処理完了後のcallback処理で`callback`を呼び出す
- 非同期処理が含まれる最も上位の呼び出し層で`async.void(function()...end)()` を呼ぶ
## JavaScriptの非同期処理理解者に向けた要点
[[JavaScript]]の非同期処理を習得済であれば以下のように理解をマッピングできる。
- `async.wrap`
- コールバック指定関数を[[Promise]]のような非同期オブジェクトを返す関数に返却する[[util.promisify]]と同じノリで使う
- `async.void(function() ... end)()`
- [[Top-Level await]]が使えない環境でエントリポイントに指定する `(async function() { ... })()` と同じノリで使う
- `async/await`のように非同期処理に関する記載を記述しない (脳内補完がいる)
## エラーの対処
### attempt to yield across C-call boundary
```error
attempt to yield across C-call boundary
```
`async.void`などでwrapせずに、`async_wrap`で生成した非同期関数を呼び出すと発生する。先ほどのコードならコマンド作成部分を以下のようにすると再現する。
```lua
vim.api.nvim_create_user_command("Hoge", function()
vim.notify("command start")
run_test()
vim.notify("command end")
end, { nargs = 0 })
```
## コメント・通知を外した最終的なコード
必要なコメント・通知以外を外したコードは以下。
```lua
local async = require("plenary.async")
local curl = require("plenary.curl")
local async_get = async.wrap(function(opts, callback)
opts.callback = callback
curl.get(opts)
end, 2)
-- query: { delay = number }
local function get_users(query)
return async_get({
url = "https://reqres.in/api/users",
query = query,
})
end
local function run_test()
vim.notify("Loading...")
local r = get_users({ delay = 1 })
vim.notify("total: " .. vim.json.decode(r.body).total)
end
vim.api.nvim_create_user_command("Hoge", async.void(run_test), { nargs = 0 })
```
### nvim_create_user_commandの補足
かなりコード量が減っているので、`nvim_create_user_command`についてだけ補足する。
```lua
vim.api.nvim_create_user_command("Hoge", function()
async.void(function()
vim.notify("command start")
run_test()
vim.notify("command end")
end)()
end, { nargs = 0 })
```
不要な`vim.notify`を削除すると以下のようになる。
```lua
vim.api.nvim_create_user_command("Hoge", function()
async.void(function()
run_test()
end)()
end, { nargs = 0 })
```
単一関数を呼び出すだけの関数は、内部で呼び出している関数に置き換えられるので
```lua
vim.api.nvim_create_user_command("Hoge", function()
async.void(run_test)()
end, { nargs = 0 })
```
また、`async.void(run_test)`も関数であり、同じことが言えるため更に
```lua
vim.api.nvim_create_user_command("Hoge", async.void(run_test), { nargs = 0 })
```
となる。
## ghostwriter.nvim の改修
もともと目的だった[[🦉ghostwriter.nvim]]の改修も完了。
<div class="link-card">
<div class="link-card-header">
<img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/>
<span class="link-card-site-name">GitHub</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<p class="link-card-title">Support async execution · tadashi-aikawa/ghostwriter.nvim@50c5f50</p>
<p class="link-card-description">A Neovim plugin to share Markdown task lists as Slack posts. - Support async execution · tadashi-aikawa/ghostwriter.nv ... </p>
</div>
<img src="https://opengraph.githubassets.com/bba1acc71bb68572e213b72fe885f745063168bea2618785993d50876c69d7e2/tadashi-aikawa/ghostwriter.nvim/commit/50c5f509ec3810cc3cb5a94aca73b0ea4aedb29e" class="link-card-image" />
</div>
<a href="https://github.com/tadashi-aikawa/ghostwriter.nvim/commit/50c5f509ec3810cc3cb5a94aca73b0ea4aedb29e"></a>
</div>