Neovimのmason.nvimとmason-lspconfig v2 setup_handlersが消えた後の設定
古い記事を見ながらmason-lspconfigの設定を書き、Neovimを起動したらattempt to call field 'setup_handlers' (a nil value)と出た。そういう人がいま増えているはずです。mason-lspconfig.nvimは2025年5月のv2.0.0でsetup_handlers()を削除しました。日本語で読める解説の多くは、まだこの関数を前提にしています。
v2の構成で分かりにくいのは、言語サーバを最終的に起動しているのが誰か、という点です。mason-lspconfigがvim.lsp.enable()を呼ぶのか、自分のinit.luaが呼ぶのか。両方で呼んだら二重に起動するのか。一時ディレクトリに環境を作り、vim.lsp.get_clients({ bufnr = 0 })の件数を数えて1つずつ確かめました。
検証環境:NVIM v0.12.4(macOS)。XDG_CONFIG_HOMEなどを一時ディレクトリに向けた空の環境に、vim.packでmason.nvim v2.3.1、mason-lspconfig.nvim v2.3.0の21コミット先(00b5473、2026-10-13)、nvim-lspconfig v2.12.0の26コミット先(ac062d0d)を入れました。サーバはmason経由のlua-language-server 3.19.2とtaplo、npmで別に入れたpyright 1.1.414です。クライアントの件数は、ファイルを開いて4秒待ってからvim.lsp.get_clients({ bufnr = 0 })で数えました。エラー文の一時ディレクトリの部分だけ省いています。
目次(9項目)
mason.nvimとmason-lspconfigの役割
名前が似た3つのプラグインが出てくるので、先に分担を整理しておきます。
| プラグイン | やること | 無いとどうなるか |
|---|---|---|
| mason.nvim | サーバ本体を~/.local/share/nvim/masonに入れ、そのbinをPATHの先頭に足す | サーバを自分で入れてPATHに通す |
| nvim-lspconfig | lsp/lua_ls.luaのような設定の実体(cmd・filetypes・root_markers)を配る | 設定を自分でlsp/に書く |
| mason-lspconfig | masonで入れたサーバをvim.lsp.enable()する。lua-language-serverとlua_lsのような名前の対応も持つ | vim.lsp.enable()を自分で呼ぶ |
入れる係がmason.nvim、有効にする係がmason-lspconfigです。設定の中身はnvim-lspconfigが持っていて、mason-lspconfigは中身をほとんど持っていません。v2のソースでlsp/に自前の設定を置いているのはpylspやelixirlsなど19個だけで、lua_lsは含まれていませんでした。
masonの役割が「PATHに足す」までだというのは、require("mason").setup()を外すと分かります。nvim-lspconfigだけ入れてvim.lsp.enable("lua_ls")を書いた状態では、vim.fn.exepath("lua-language-server")が空になり、クライアントは0件でした。起動時には何のメッセージも出ません。:checkhealth vim.lspを開いて初めて次の行が見つかります。
- ⚠️ WARNING 'lua-language-server' is not executable. Configuration will not be used.
mason.setup()を呼ぶとvim.env.PATHの先頭に…/nvim/mason/binが入り、mason-lspconfigが無くてもvim.lsp.enable("lua_ls")だけでアタッチしました。
v2で消えたもの(setup_handlersとautomatic_installation)
リポジトリに同梱のCHANGELOG.mdは、v2.0.0で削除したものを次のように書いています。
Remove the
handlerssetting and.setup_handlers()function. It has been replaced by the new nativevim.lsp.config()API and a newautomatic_enablesetting.Remove the
automatic_installationsetting. This feature is no longer compatible with the new native LSP configuration mechanism.
v1時代の書き方をそのままv2に読ませると何が起きるのか、3通り試しました。結果はそれぞれ違います。
| v1の書き方 | v2で起きたこと | lua_ls |
|---|---|---|
.setup_handlers({...}) | 起動時にE5113。init.luaはその行で止まる | 1件(先にsetup()を呼んでいれば) |
setup({ handlers = {...} }) | エラーなし。関数は一度も呼ばれない | 1件 |
setup({ automatic_installation = true }) | エラーなし。値は保存されるが誰も読まない | 1件 |
1つ目は、Zennなどの移行記事によく載っている次の形です。
require("mason").setup()
require("mason-lspconfig").setup()
require("mason-lspconfig").setup_handlers({
function(server_name)
require("lspconfig")[server_name].setup({})
end,
})
起動するとこう出ます。
Error in …/nvim/init.lua:
E5113: Lua chunk: …/nvim/init.lua:4: attempt to call field 'setup_handlers' (a nil value)
ややこしいのはここからです。このエラーが出ていても、lua_lsはアタッチしていました。直前のsetup()がautomatic_enableを既定のまま動かし、masonで入れたlua_lsを有効にしているからです。エラーで止まったのはinit.luaの残りの行で、setup_handlersより下に書いたキーマップやオプションが読まれていません。LSPが動いているので見逃しやすく、別の設定が効かなくなったように見えます。
2つ目のhandlersは黙って無視されます。ハンドラの中でvim.gに印を付けるようにして起動しましたが、印はnilのままでした。3つ目のautomatic_installation = trueもエラーにはならず、require("mason-lspconfig.settings").currentを覗くとtrueが入っています。設定値をそのままマージするだけで検証していないからです。v2のソースをgrepしても、この名前を読んでいる箇所は1つもありませんでした。開いたファイルの言語のサーバを勝手に入れる動きは、もう起きません。
今の最小構成(vim.pack + mason + mason-lspconfig)
Neovim 0.12なら、標準のプラグイン管理vim.packだけで揃います。v2の要件はNeovim 0.11以上、mason.nvim 2.0.0以上、nvim-lspconfig 2.0.0以上です。
-- ~/.config/nvim/init.lua
vim.pack.add({
"https://github.com/neovim/nvim-lspconfig",
"https://github.com/mason-org/mason.nvim",
"https://github.com/mason-org/mason-lspconfig.nvim",
})
require("mason").setup()
require("mason-lspconfig").setup({
ensure_installed = { "lua_ls" },
})
mason.setup()はmason-lspconfigより先に呼びます。nvim-lspconfigはruntimepathに載っていれば足り、setup()は要りません。
サーバを入れるのは:MasonInstall lua-language-server(masonのパッケージ名)か、上のensure_installed(lspconfigの名前)です。消えたautomatic_installationの代わりに使えるのは後者で、起動時に足りないものだけを入れます。ensure_installed = { "taplo" }にしてa.tomlを開いた状態で起動すると、しばらくして[mason-lspconfig.nvim] taplo was successfully installedと出て、再起動しないまま開いているバッファにtaplo(TOMLのサーバ)がアタッチしました。ソースにはnot platform.is_headlessという条件があり、nvim --headlessでは走りません。
動いているかは:checkhealth vim.lspで見ます。lua_lsを開いた状態の出力の一部です。
vim.lsp: Active Clients ~
- lua_ls (id: 1)
- Version: 3.19.2
- Root directory: nil
- Command: { "lua-language-server" }
見方は:checkhealthの使い方にまとめてあります。
automatic_enableは誰のvim.lsp.enableを呼んでいるか
automatic_enableの中身は短いので、ソースを読むのがいちばん早いです。setup()の時点でmasonに入っているパッケージを全部回し、lspconfigの名前に変換できたものについてvim.lsp.enable(name)を呼びます。あとからmasonでインストールが成功したときも、同じ処理がイベントで走ります。呼んでいるのはNeovim本体のvim.lsp.enable()そのもので、自分のinit.luaに書くのと何も変わりません。
そうなると、mason-lspconfigに任せたうえで自分でもvim.lsp.enable("lua_ls")を書いた場合が気になります。2通りの順番で試しました。
| init.luaの書き方 | クライアント数 |
|---|---|
setup()だけ | 1 |
setup()の後にvim.lsp.enable("lua_ls") | 1 |
vim.lsp.enable("lua_ls")の後にsetup() | 1 |
automatic_enable = false | 0 |
setup()を呼ばない | 0 |
どちらの順番でも1件で、バッファで絞らないvim.lsp.get_clients({})も1件でした。vim.lsp.enable()は設定名ごとに「有効」の印を立てる関数で、同じ名前を2回有効にしても印が2つになるわけではありません。起動はファイルタイプが一致したバッファごとに1回で、ルートが同じなら既存のクライアントが使い回されます。vim.lsp.is_enabled("lua_ls")で印の状態を確かめられます。
最後の行は見落としやすいところです。masonでサーバを入れてあっても、setup()を呼ばなければ何も有効になりません。lazy.nvimならopts経由で呼ばれますが、vim.packでは自分で書きます。
一部のサーバだけ外す(exclude)と自分で書く設定の当て方
masonで入れてはあるけれど、勝手に有効にされたくないサーバがあるときはexcludeを使います。
require("mason-lspconfig").setup({
automatic_enable = { exclude = { "lua_ls" } },
})
この状態でa.luaを開くとクライアントは0件、vim.lsp.is_enabled("lua_ls")もfalseでした。続けて同じinit.luaにvim.lsp.enable("lua_ls")を足すと1件に戻ります。excludeは「mason-lspconfigが呼ばない」というだけで、起動を禁止する設定ではありません。外す理由が「別のタイミングで自分で有効にしたいから」なら、この組み合わせで足ります。
v1でハンドラの中に書いていたサーバごとの設定は、vim.lsp.config()に移します。
vim.lsp.config("lua_ls", {
settings = {
Lua = { diagnostics = { globals = { "vim" } } },
},
})
これをsetup()の前に置いた場合と後に置いた場合の両方で、起動したクライアントのconfig.settingsを出しました。結果はどちらも同じです。
{
Lua = {
codeLens = {
enable = true
},
diagnostics = {
globals = { "vim" }
},
hint = {
enable = true,
semicolon = "Disable"
}
}
}
codeLensとhintはnvim-lspconfigのlsp/lua_ls.luaから来た既定値で、diagnosticsが自分で足したものです。置き換えではなくマージされています。順番に関係なく効くのは、vim.lsp.config()が設定を貯めておくだけで、組み立てはクライアントを起動する瞬間だからです。lsp/ディレクトリに置く書き方はNeovimの標準LSP設定(lsp/とvim.lsp.enable)で扱っていて、mason経由で有効にされたサーバにもそのまま効きます。
masonの外で入れたサーバの扱い
pyrightをmasonではなくnpmで入れ、そのbinをPATHに通してからa.pyを開きました。setup()だけの構成ではクライアントは0件です。READMEにも「Mason経由で入れたサーバだけを有効にし、ほかの場所に入れたサーバは認識しない」と書かれています。
init.luaにvim.lsp.enable("pyright")を1行足すと、pyright-langserver --stdioで起動して1件になりました。brewやnpm、言語のツールチェーンで入れたサーバは、自分で有効にします。
require("mason").setup()
require("mason-lspconfig").setup() -- masonで入れたものはこちらが有効にする
vim.lsp.enable("pyright") -- masonの外で入れたもの
同じ名前のコマンドがmasonと外の両方にある場合は、masonのbinがPATHの先頭に入るのでmason側が使われます。PATHの前のほうに偽のlua-language-serverを置いて起動しても、vim.fn.exepath("lua-language-server")は…/nvim/mason/bin/lua-language-serverを返しました。:checkhealth vim.lspのCommand欄はコマンド名しか出さないので、どちらが動いているかを知りたいときはexepath()で実際のパスを見ます。
v1のまま固定したいとき
すぐに書き換える時間が無いなら、mason-lspconfigをv1.32.0に、masonをv1.11.0に固定する手があります。vim.packではversionで指定します。
vim.pack.add({
"https://github.com/neovim/nvim-lspconfig",
{ src = "https://github.com/mason-org/mason.nvim", version = "v1.11.0" },
{ src = "https://github.com/mason-org/mason-lspconfig.nvim", version = "v1.32.0" },
})
この状態で先ほどのsetup_handlersの設定を読ませると、エラーにはならずlua_lsもアタッチしました。ただし起動すると次の警告が出て、Press ENTERで止まります。
The `require('lspconfig')` "framework" is deprecated, use vim.lsp.config (see :help lspconfig-nvim-0.11) instead.
Feature will be removed in nvim-lspconfig v3.0.0
出しているのはmason-lspconfigではなく、nvim-lspconfigの最新版です。ハンドラの中のrequire("lspconfig")[server_name].setup({})がもう推奨されない書き方で、v3.0.0で消えると予告されています。固定で延命できるのはそこまでで、setup_handlersのブロックを消して設定をvim.lsp.config()へ移すほうが手間は少なく済みます。
検証の最初にsetup_handlersの設定を読ませたとき、E5113が出たのでLSPは動いていないものと決めつけ、クライアント数を数える前にサーバのインストールからやり直そうとしていました。出力を見直すと、エラーの真下にclients=1と出ていました。v1の固定でも似たことがあり、警告でPress ENTERが出たのでmason-lspconfigの古い版が嫌われているのかと思ったら、出していたのはnvim-lspconfigのほうでした。どちらも、出たメッセージの発信元と実際の状態を分けて見ないと判断を誤るところでした。
まとめ
v2のmason-lspconfigがやることは、masonで入れたサーバの名前を変換してvim.lsp.enable()を呼ぶことだけです。サーバの起動はNeovim本体、設定の中身はnvim-lspconfigと自分のvim.lsp.config()が受け持ちます。setup_handlersはエラーになり、handlersとautomatic_installationは黙って無視されるので、v1の設定を持ち越しているならまずこの3つを消します。
vim.lsp.enable()を重ねて呼んでも二重には起動せず、excludeは起動の禁止ではありません。保存時のフォーマットをLSPに任せるかどうかはconform.nvimとLSPの使い分けで扱っています。