NeovimのLSPのFormatter設定を見直す

Neovim v0.10でLSPのcapabilitiesのDynamic registrationが可能になりました。日本語に訳すなら「動的にLSPの機能を登録できるようになった」といったところでしょうか。

「何がうれしいのか」「これを受けて筆者が変更した設定」を紹介します。
特にclient.server_capabilitiesやBiomeを使っている人は読むといいかもしれません。

先にまとめ

  • BiomeのFormatterもlspconfigだけでOK
  • LSPの機能が使えるかの判定にはclient.supports_method
  • on_attachから必要に応じてLspAttachへ移行しよう
  • Formatterの無効化はvim.lsp.buf.formatでも可能

何がうれしいのか

今まではFormatterを動的に登録するLSPを、LSPとFormatterで別々に設定していました。
たとえば、Biomeは次のようにnvim-lspconfigでLSP、none-ls.nvimでFormatterを動かしていました。

今まで
local lspconfig = require("lspconfig")
lspconfig.biome.setup({
-- 設定
})
local null_ls = require("null-ls")
local sources = {
-- ほかのFormatterなど
null_ls.builtins.formatting.biome,
}
null_ls.setup({
sources = sources,
})

v0.10からはlspconfigでまとめて動かせます。

これから
local lspconfig = require("lspconfig")
lspconfig.biome.setup({
-- 設定
})
local null_ls = require("null-ls")
local sources = {
-- ほかのFormatterなど
null_ls.builtins.formatting.biome,
}
null_ls.setup({
sources = sources,
})

他のLSPではないFormatterなどは、引き続きnone-ls.nvimなり他のプラグインを通す必要があります。

動的に登録されたFormatterを保存時に実行

Biomeのように動的に登録されたFormatterを実行しようとしたらそのままでは動きませんでした。いったん修正前の設定を載せます。次の項目から小分けで解説します。

今まで
local on_attach = function(client, bufnr)
local bufopts = { noremap = true, silent = true, buffer = bufnr }
vim.bo[bufnr].omnifunc = "v:lua.vim.lsp.omnifunc"
vim.keymap.set("n", "gD", "<cmd>lua vim.lsp.buf.declaration()<CR>", bufopts)
-- その他キーマップ
if client.server_capabilities.documentFormattingProvider then
vim.api.nvim_create_autocmd({ "BufWritePre" }, {
group = "my_nvim_rc",
buffer = bufnr,
callback = function()
vim.lsp.buf.format({ timeout_ms = 2000 })
end,
})
vim.api.nvim_create_user_command("Format", function()
vim.lsp.buf.format({ timeout_ms = 2000 })
end, {})
end
end
local lspconfig = require("lspconfig")
lspconfig.biome.setup({
on_attach = on_attach,
-- その他設定
})
local mason_lspconfig = require("mason-lspconfig")
mason_lspconfig.setup_handlers({
function(server_name)
local opts = {}
opts.on_attach = on_attach
-- その他設定
lspconfig[server_name].setup(opts)
end,
})

Formatter自体は有効であることを確認

まず、Formatter自体は次のコマンドで問題なく動くことを確認できました。

:lua vim.lsp.buf.format()

server_capabilitiesを書き換える

client.server_capabilitiesはLSPが初期化時に設定された機能しか含まれません(参考:Pull Request #23681)。
つまり、動的に登録した機能はclient.server_capabilitiesからは参照できません。

実際に次のコマンドで確認してみます。

:lua vim.print(vim.lsp.get_clients({name="biome"})[1].server_capabilities)

結果を見ると、Formatterに関する記述がありませんね。

{
codeActionProvider = true,
positionEncoding = "utf-16",
textDocumentSync = {
change = 2,
openClose = true,
save = {
includeText = false
},
willSave = false,
willSaveWaitUntil = false
}
}

動的に登録された機能はclient.dynamic_capabilitiesで確認できます。

:lua vim.print(vim.lsp.get_clients({name="biome"})[1].dynamic_capabilities)

結果を見るとtextDocument/formattingがありました!

{
capabilities = {
["textDocument/formatting"] = { {
id = "biome_formatting",
method = "textDocument/formatting"
} },
-- ...
}

client.server_capabilitiesclient.dynamic_capabilitiesに分かれており、判定のために両方を確認するのは少々面倒です。

これを一括で確認できるAPIclient.supports_methodが用意されています。
そのため、次のように書き換えます。

if client.server_capabilities.documentFormattingProvider then

on_attachからLspAttachへ

上記のclient.supports_methodへ書き換えても、まだ保存時にFormatterは実行されませんでした。

これだと動かない
local on_attach = function(client, bufnr)
-- アタッチ時の処理
if client.supports_method("textDocument/formatting") then
-- Formatterの実行
end
end
local lspconfig = require("lspconfig")
local mason_lspconfig = require("mason-lspconfig")
mason_lspconfig.setup_handlers({
function(server_name)
local opts = {}
opts.on_attach = on_attach
-- その他設定
lspconfig[server_name].setup(opts)
end,
})

lspconfigのon_attachの実装を見ると、on_attachが実行されるのはバッファに入った後(BufEnter)のようです。

バッファにLSPがアタッチされたタイミングで発火するイベントLspAttachが用意されているのでこれを使って書き換えます。

これから
vim.api.nvim_create_autocmd("LspAttach", {
group = "my_nvim_rc",
callback = function(ev)
local bufopts = { noremap = true, silent = true, buffer = ev.buf }
vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc"
vim.keymap.set("n", "gD", "<cmd>lua vim.lsp.buf.declaration()<CR>", bufopts)
-- キーマップが続く…
local client = vim.lsp.get_client_by_id(ev.data.client_id)
if client == nil then
return
end
if client.supports_method("textDocument/formatting") then
vim.api.nvim_create_autocmd({ "BufWritePre" }, {
group = "my_nvim_rc",
buffer = ev.bufnr,
callback = function()
my_format() -- 後述
end,
})
vim.api.nvim_create_user_command("Format", function()
my_format()
end, {})
end
end,
})

今後のon_attachはLSPごとに固有の設定をしたいときに使うことになりそうです。

ファイル保存時にFormatを無効にする

ここまではファイル保存時にLSPのFormatterを動かす方法を伝えました。しかし、LSPによってはFormatterを無効にしたくなるかもしれません。

以前はLSPごとに切り分けられるon_attachを使って次のように書いていました。

今まで
local function on_attach_disable_format(client, buffer)
client.server_capabilities.documentFormattingProvider = false
client.on_attach = on_attach
end
local lspconfig = require("lspconfig")
local mason_lspconfig = require("mason-lspconfig")
mason_lspconfig.setup_handlers({
function(server_name)
local opts = {}
opts.on_attach = on_attach
if server_name == "server name" then
opts.on_attach = on_attach_disable_format
-- その他設定
lspconfig[server_name].setup(opts)
end,
})

これでも良かったのですが、このclient.server_capabilitiesだと動的に登録されるFormatterの場合は無効にできません。いちいち動的なのか調べて対応するのは面倒です。

そこで、vim.lsp.buf.formatの引数filterを使います。

local function my_format()
vim.lsp.buf.format({
timeout_ms = 2000,
filter = function(client)
return client.name ~= "tsserver" and client.name ~= "foobar"
end,
})
end

filterの引数はLSPのクライアントvim.lsp.Clientであるため、名前でLSPを判定できます。

まとめ

最後にもう一度まとめです。

  • BiomeのFormatterもlspconfigだけでOK
  • LSPの機能が使えるかの判定にはclient.supports_method
  • on_attachから必要に応じてLspAttachへ移行しよう
  • Formatterの無効化はvim.lsp.buf.formatでも可能

今まで更新をサボってきたツケが回ってきた感がありますが、楽しかったので良しとします。