neovim-lspconfig + Biome で保存時に import の整理をする
3行まとめ
- neovim-lspconfig で Biome の LSP Proxy を起動できる
-
textDocument/codeAction
でsource.organizeImports
とsource.fixAll
を実行することで、import の整理が可能 - codeAction の同期実行は gopls のドキュメントを参考にする
前提1: Biome と import
Biome には import に関連する機能が2つ存在する。
- Analyzer の Imports Sorting
- import をいい感じに並び替えてくれる機能
-
eslint-plugin-import の
order
rule に近い
-
eslint-plugin-import の
- デフォルトで有効になっている
- import をいい感じに並び替えてくれる機能
- Lint の
noUnusedImports
rule- 「未使用の import を許可しない」という Lint ルールで、 safe fix 対応
-
eslint-plugin-unused-imports の
no-unused-imports
rule と同等
-
eslint-plugin-unused-imports の
- Recommended ルールセットに含まれておらず、自分で有効化が必要なケースが多そう
-
{ "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", "linter": { "enabled": true, "rules": { "recommended": true, + "correctness": { + "noUnusedImports": "error" + } } } }
- 「未使用の import を許可しない」という Lint ルールで、 safe fix 対応
前提2: neovim-lspconfig と Biome
Biome は lsp-proxy
というコマンドを持っており、バックグラウンドで立ち上げた long-running な Biome プロセスに対して LSP(Language Server Protocol) でやり取りすることができる。
Neovim で Biome を動かす際、neovim-lspconfig を使う場合はこの biome lsp-proxy
を使うことになる。
保存時に import の整理をする
保存時に Biome によるフォーマットを動かすのであれば、そのとき import の並び替えおよび未使用 import の削除もまとめてやってほしい。しかし、vim.lsp.buf.format()
だけではそこまではやってくれない。これは LSP 的にはフォーマットとこれらの処理は別ものになっているのが理由。
Biome LSP Proxy 経由で import の整理をするには、textDocument/codeAction
で source.organizeImports
と source.fixAll
を実行する必要がある。
-
source.organizeImports
は import の並び替え -
source.fixAll
は未使用 import の削除- (おそらく
biome lint
相当の処理が動いてるので、他にもいろいろ修正される)
- (おそらく
Neovim にはいまのところ textDocument/codeAction
を同期的に実行する直接的な API は存在せず、ちょっと低レベルな関数で実現することになる。
Go の Language Server である gopls のドキュメントでその実装が紹介されており、それが参考になる。
筆者は以下のようなコードを実行することで、保存時(BufPreWrite
) に format に加えて textDocument/codeAction
の source.organizeImports
と source.fixAll
を動かすようにした。
---@param client vim.lsp.Client
---@param bufnr integer
---@param cmd string
local function code_action_sync(client, bufnr, cmd)
-- https://github.com/golang/tools/blob/gopls/v0.11.0/gopls/doc/vim.md#imports
local params = vim.lsp.util.make_range_params()
params.context = { only = { cmd }, diagnostics = {} }
-- gopls のドキュメントでは `vim.lsp.buf_request_sync` を使っているが、
-- ここでは対象 Language Server を1つに絞るために `vim.lsp.Client` の `request_sync` を使う
local res = client.request_sync("textDocument/codeAction", params, 3000, bufnr)
for _, r in pairs(res and res.result or {}) do
if r.edit then
local enc = (vim.lsp.get_client_by_id(cid) or {}).offset_encoding or "utf-16"
vim.lsp.util.apply_workspace_edit(r.edit, enc)
end
end
end
---@param client vim.lsp.Client
---@param bufnr integer
local function organize_imports_sync(client, bufnr)
code_action_sync(client, bufnr, "source.organizeImports")
end
---@param client vim.lsp.Client
---@param bufnr integer
local function fix_all_sync(client, bufnr)
code_action_sync(client, bufnr, "source.fixAll")
end
---@type table<string, fun(client: vim.lsp.Client, bufnr: integer)[]>
local save_handlers_by_client_name = {
gopls = { organize_imports_sync, format_sync },
biome = { fix_all_sync, organize_imports_sync, format_sync },
}
-- none-ls を含むすべての Language Server の保存時の処理をまとめてしまう
--
-- Language Server の処理を連続で呼び出すと意図通りの動作をしないことがある
-- Server 側の内部状態の更新が間に合わないのか?
-- その回避のために sleep が必要
--
-- かつ、複数 Language Server にリクエストを送るときにも sleep を入れるために
-- 1つの BufWritePre にまとめている
vim.api.nvim_create_autocmd("BufWritePre", {
---@param args { buf: integer }
callback = function(args)
local bufnr = args.buf
local shouldSleep = false
for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do
local save_handlers = save_handlers_by_client_name[client.name]
for _, f in pairs(save_handlers or {}) do
if shouldSleep then
vim.api.nvim_command("sleep 10ms")
else
shouldSleep = true
end
f(client, bufnr)
end
end
end,
})
Discussion