Neovimとcoc.nvim構成でコード保存時にdeno fmtを実行する

2023/11/07に公開

はじめに

自分はTypeScript(Node.js)でサーバサイドをNeovimで書くことが多く、補完やコード整形にcoc.nvimとそのプラグインであるcoc-tsservercoc-prettierを利用しています。

最近趣味でNeovimでDenoを書いていると、.ts拡張子が同じため、保存時にPrettierが実行されてしまう問題がありました。これは自分のNeovimの設定の影響です。DenoはCLI自体にビルドインのフォーマッターが搭載されており、保存時にdeno fmtが実行されて欲しいです。

init.luaから一部抜粋
vim.api.nvim_command("autocmd BufWritePre *.ts,*.tsx :Prettier")
command! -nargs=0 Prettier :CocCommand prettier.forceFormatDocument

init.luaの詳細

coc.nvimは、以下のようにプロジェクトルートに設定ファイルを用意すれば、tsserverやprettierを無効にしてdenoのLSPを有効にすることが出来ます。これでPrettierが実行されることは無くなります。

.vim/coc-settings.json
{
  "deno.enable": true,
  "deno.lint": false,
  "deno.unstable": true,
  "deno.config": "deno.json",
  "tsserver.enable": false,
  "prettier.enable": false
}

もちろんこれではコード整形が何も走らないので開発体験が悪いです。本記事ではこの状態からdenoのファイル保存時にdeno fmtが走るようにするところを目指します。

プラグイン開発

shuntaka9576/deno-fmt.vimというプラグインを作ってみました。これはcoc.nvimのLSPの利用状態をチェックして、denoのLSPが引っ掛かったら、deno fmtを実行するというものです。denops.vimを利用しています。

この処理で、denoが存在しない場合は何もしません。なので普通にTS(Node.js)でコードを書いている場合は何もしません。(実行すらしないのが理想ですが、方法がわからず、こうしています)
https://github.com/shuntaka9576/deno-fmt.vim/blob/aab834a581e6f1bb37ba992d2ce566c8eaec12bd/denops/deno-fmt/main.ts#L39-L44

denoのLSPの起動が確認できたら、バッファの内容を取り出しtmpファイルに書き出しdeno fmtで整形します。
https://github.com/shuntaka9576/deno-fmt.vim/blob/aab834a581e6f1bb37ba992d2ce566c8eaec12bd/denops/deno-fmt/main.ts#L49-L73

整形済みのdenoファイルと整形していないdenoファイルでdiffをとり、差分をもとにバッファを書き換えます。undo redoログが変にならないように、batch.collectでアトミックかつシーケンシャルに処理します。
https://github.com/shuntaka9576/deno-fmt.vim/blob/aab834a581e6f1bb37ba992d2ce566c8eaec12bd/denops/deno-fmt/main.ts#L74-L109

こんな感じで整形されます。

そのほか

  • echo 'ソースコード' | deno fmt -で標準入力を使う方法を考えましたが、おそらくエスケープまわりで苦戦し見送りました。
  • NO_COLOR=0 deno fmt --check差分出力が可能ですが、表示が正しくないケースがあり、見送りました。
  • .vim/coc-settings.jsonがモノレポに対応しているわけではないので、フロントエンドとDenoのプロジェクトが混合している場合は、手間ですが、オプションを都度反転させる必要があります。
cat .vim/coc-settings.json | jq '.["tsserver.enable"] = (.["tsserver.enable"] | not) | .["prettier.enable"] = (.["prettier.enable"] | not) | .["deno.enable"] = (.["deno.enable"] | not)' | tee .vim/coc-settings.json

最後に

TS(Node.js)を書いているときの保存時のオーバーヘッドが高くなった気もしており、改善の余地がありそうです。また、もっと楽にできる方法はありそうな気はしています。。

参考

vim-goimportsから、diffをとってバッファを書き換える方法を知りました。他にもいろんな配慮があり、よりソースを読んで理解を深めたいです。

余談

DenoFestに参加してからDenoのモチベーションが高く、自分のブログのサーバサイド(APIGateway Lambda構成)をDeno Deployに置き変える活動をしようとしていました。

Discussion