NeovimのLSPが動かない原因はプラグインではなくPATHだった

NeovimのLSPが動かない。そんな時、私はまた別のプラグインを探しかけました。補完が弱ければ追加、C#が動かなければ追加。やっているうちに、何が起動しているのか余計に分からなくなります。

今回詰まったのは、プラグインの数ではありませんでした。Roslynはどの.NETを使うのか。Ruffはどこから来たものか。WSL2のLSPがWindows側の実行ファイルを見ていないか。問題はPATHでした。

そこで、既存のLazyVim設定は残しました。追加する前に実行経路を整理し、C#、TypeScript、Python、Go、Shell、JSONでLSPが接続するところまで確認する。SQLは無理にLSPを足さない。これが今回の結論です。

先に、ツールごとの役割を決めた

コマンドをインストールする道具と、インストール済みのコマンドを呼び出す道具を分けました。同じコマンドをmise・Mason・uvなど複数の管理元から入れないよう、どの道具が何を管理するかを先に決めました。

道具 役割
mise .NET SDK、Node.js、Go、Python本体
Mason Roslyn Language Server、netcoredbg、Bash Language Serverなど
uv Pythonプロジェクトの環境、Ruff
Conform フォーマッターの呼び出し
nvim-lint リンターの呼び出し
LazyVim プラグイン読込と既存のキーマップ基盤

例えばRuffはuvで管理しているものを使い、Masonからは追加しませんでした。入っていれば安心、ではなく、どちらが起動するかを一つに絞ります。

nvim-lspconfigの公式ドキュメントでも、まず実行コマンドがPATH上にあるかを確認します。設定を増やす前に、ここを見るべきでした。

C#はOmniSharpとRoslynを同時に起動しない

LazyVimの.NET用extraを有効にすると、OmniSharpの設定が入ります。今回はRoslyn Language Serverへ寄せました。

同じC#ファイルへ二つのLanguage Serverを付ける必要はありません。OmniSharpは消さず、起動だけ止めて戻せるようにしました。roslyn.nvimも使わず、nvim-lspconfigのroslyn_lsを設定しています。

さらに、mise管理の.NETはシステム標準とは別の場所にあります。Roslynの子プロセスまで同じSDKを使うよう、Neovim側でDOTNET_ROOTとPATHを明示しました。

cmd_env = {
  DOTNET_ROOT = "<mise-managed-dotnet-root>",
  DOTNET_ROOT_X64 = "<mise-managed-dotnet-root>",
  PATH = "<mise-managed-dotnet-root>:<mason-bin>:" .. vim.env.PATH,
}

nvim-lspconfigのRoslyn設定も、実行ファイルをPATHから見つける構成です。私の環境では、miseの.NETをRoslynの子プロセスまで渡す必要がありました。

上は、RoslynのLSP設定へ起動環境を渡す部分の抜粋です。既存の環境設定へ組み込み、<mise-managed-dotnet-root>と<mason-bin>を実際の場所へ置き換えます。Neovimを起動したシェルと、Roslynの子プロセスが見る.NETの場所を、ここで揃えました。

ShellとPythonも、暗黙のPATHに任せなかった

WSL2にはWindows側のPATHも混ざります。Bash Language Serverが意図しない実行ファイルを選ばないよう、Masonの実体とNode.js本体をPATHへ明示しました。

Pythonでは、Ruffのコマンドを~/.local/bin/ruffまたはPATH上のRuffから解決する設定にしました。formatterはConformからruff_organize_importsとruff_formatを呼び、Shellはshfmt、lintはShellCheckを使います。

使う実行ファイルと、その起動環境を明示して揃えました。

TypeScript、Go、JSONは既存設定を活かせた

TypeScriptのvtsls、Goのgopls、JSONのjsonlsは既存設定のまま接続できました。問題が出ていない言語まで触ると、直した範囲がぼやけます。この三つは接続確認だけにして、設定を増やしていません。

SQLの汎用LSPは無理に採用しなかった

SQL用にsqlsも試しました。しかしGo経由でのビルドが長時間終わらず、実行可能ファイルができない状態でした。

SQL対応の欄を埋めるためだけに、別のLSPを足すのはやめました。現時点ではTreesitter、SQLFluff、DB操作用UIを残し、汎用SQL LSPは保留です。

使えないものを設定へ残すと、後から見た時に「導入済みなのか、壊れているのか、意図的に外したのか」が分からなくなります。採用しなかった判断も設定の一部だと思っています。

起動できることと、実際に付くことを分けて確認した

設定後は、Neovimのheadless起動だけでは終わらせませんでした。C#、TypeScript、Python、Go、Shell、JSONの各テストファイルを開き、対象のLSPが接続することを確認しました。

確認できた構成は次のとおりです。

  • C#:roslyn_ls
  • TypeScript:vtsls
  • Python:pyrightとruff
  • Go:gopls
  • Shell:bashls
  • JSON:jsonls

LSPが付かない時は、対象ファイルを開いて:LspInfoを見ます。そこで接続中のサーバー名とroot directoryを確認し、次に:checkhealth vim.lsp、最後にシェルでcommand -v <language-server>。いまはこの順番です。

ここで終わりにしたい状態は、コマンドが見つかることだけではありません。開いた言語のファイルに、使う予定のサーバーが接続していることです。command -vで出たパスがWindows側や別の管理元なら、どの実体を起動するかから見直します。接続済みでもroot directoryが想定したプロジェクトを指しているかは確認します。

対象のファイルを開く、:LspInfo、:checkhealth vim.lsp、シェルで実行元を確認
対象ファイルの接続状態から、実行ファイルの場所へ確認を進めます。

headless起動でエラーがないことに加え、対象言語のテストファイルへLSPが接続することも確かめました。起動確認と、実際に開いたファイルでの接続確認をセットにしました。

Neovimの黒い画面を格好よくしたい気持ちはあります。でも、その前に補完、定義ジャンプ、フォーマット、診断が静かに動いてほしい。土台が固まり、ようやくgdやgrでコードを追うこと自体を楽しめるようになりました。

この環境をWindowsとWSL2へどう分けたかはAIにコードを書かせる時代に、WezTerm・WSL2・Neovimを整えた理由、実際にgdやgrを使う流れはAIにコードを書かせた後、rg・fzf・Yazi・Neovimで読むへ書いています。

確認した情報

確認日: 2026年9月26日

  • nvim-lspconfig:サーバーの導入、実行可能なPATH、設定・接続の確認
  • Neovim LSP:LSPの接続とヘルスチェック

掲載図は、本文の「対象ファイル、LspInfo、checkhealth、実行元」という確認順をまとめたものです。

コメント

タイトルとURLをコピーしました