NeovimのLSPスタックを「自作」せよ:lspconfigの呪縛を解き、極限のレスポンスを手に入れる
多くのエンジニアは `nvim-lspconfig` をインストールし、その恩恵を享受して満足する。しかし、アーキテクトの視点から言えば、それは「ブラックボックスを背負って走る」行為に過ぎない。設定の肥大化、非同期イベントの衝突、そして不要なCapabilityのネゴシエーション。これらがエディタの起動速度と入力遅延を蝕んでいることに、気づいているだろうか。
今日、我々が挑戦するのは、`nvim-lspconfig` という既製品の「杖」を捨て、Luaの `vim.lsp.start()` APIを直接叩くことで、「君が必要な機能だけを持つ、世界で最も軽量な言語専用IDE」を構築する実験だ。
1. なぜ「自作」なのか:LSPの内部構造を知る
LSP(Language Server Protocol)は、JSON-RPC 2.0という単純なプロトコルで動く。Neovimにおいて、`vim.lsp.start()` は以下の3つのタスクを実行する低レイヤAPIである。
1. プロセス起動: 指定したサーバーコマンドを子プロセスとして立ち上げる。
2. ハンドシェイク: `initialize` リクエストを送り、サーバーとクライアントのCapabilityをネゴシエートする。
3. イベントループの接続: サーバーからの `textDocument/publishDiagnostics` 等の通知を、NeovimのLuaコールバックへルーティングする。
これらをラップする既存プラグインは「汎用性」のために膨大な条件分岐を抱えている。特定の言語(例えばRustやGo)に絞り、必要なCapabilityをハードコードすれば、起動時のオーバーヘッドは極限までゼロに近づく。
2. 最小構成のLSPクライアント実装
以下のコードを `lua/lsp_minimal.lua` として作成せよ。これは、`gopls` を例にした最小実装だ。
— 最小構成のLSP起動スクリプト
local function start_gopls()
vim.lsp.start({
name = ‘minimal-gopls’,
cmd = {‘gopls’}, — サーバーのバイナリパス
root_dir = vim.fs.dirname(vim.fs.find({‘go.mod’, ‘.git’}, { upward = true })[1]),
capabilities = {
— 必要な機能だけを明示的に指定することで、サーバー側の不要な計算を抑制する
textDocument = {
completion = { completionItem = { snippetSupport = true } },
hover = { contentFormat = {‘markdown’, ‘plaintext’} }
}
},
on_attach = function(client, bufnr)
— ここで必要最小限のキーバインドを定義する
— 汎用的な設定を読み込む必要はない。ここに書くだけでいい。
vim.keymap.set(‘n’, ‘gd’, vim.lsp.buf.definition, { buffer = bufnr })
vim.keymap.set(‘n’, ‘K’, vim.lsp.buf.hover, { buffer = bufnr })
end
})
end
— Goファイルを開いた時に自動起動するAutocmd
vim.api.nvim_create_autocmd(‘FileType’, {
pattern = ‘go’,
callback = start_gopls,
})
この実装の真骨頂は、「不必要なCapabilityを通知しない」ことにある。例えば、`documentFormatting` を含めなければ、サーバーはフォーマット計算をサボる。これによるメモリ消費の削減とCPU負荷の低下は、大規模プロジェクトにおいて無視できない差を生む。
3. CI/CDパイプラインとの高度な連携:コンテナ内開発の自動化
伝説的なDevOps環境では、IDEは「コンテナ環境」そのものと同期している必要がある。Dockerコンテナ内でLSPを動かす場合、Neovimはホスト側、サーバーはコンテナ側という構成が一般的だが、ここでは「コンテナ自体をエディタのバックエンドとする」手法を推奨する。
Docker-LSP Proxyの構築
`docker exec` をラッパーとして使い、LSPサーバーをコンテナ内の環境と完全にシンクロさせるための設定だ。
!/bin/bash
lsp-wrapper.sh
ホストのnvimから呼ばれ、コンテナ内の環境でgoplsを実行する
docker exec -i my-dev-container gopls
これを `cmd = {‘/path/to/lsp-wrapper.sh’}` とすることで、コンテナ内部の `GOPATH` やライブラリ構成と、NeovimのLSPクライアントが完全に一致する。これにより、「ローカル環境のビルドエラー」という無駄なデバッグ時間を根絶できる。
4. 内部アーキテクチャの最適化ハック
さらに深淵に触れよう。NeovimのLSPクライアントのメモリ消費を抑えるための秘策は、「RPCメッセージのフィルタリング」にある。
LSPサーバーは、時として数メガバイトのJSONを送りつけてくる(特に大規模なシンボル検索時)。`vim.lsp.set_log_level(‘OFF’)` を設定するのは当然として、以下のハックを適用せよ。
- 静的解析のオフロード: `semanticTokens`(シンタックスハイライトのLSP化)は、低スペックな環境では即座に無効化せよ。これは `capabilities.textDocument.semanticTokens = nil` とするだけでいい。
- バッファ同期の最適化: `didChange` 通知の頻度を `vim.lsp.buf_attach_client` の設定で調整し、入力ごとの送信を抑制する。
結びに:エンジニアの誇りとして
「ライブラリに頼る」ことは、開発速度を上げるための手段であって、目的ではない。自らの手で `vim.lsp.start()` を呼び出し、プロトコルのハンドシェイクを理解し、サーバーとの対話を最適化する。
この「底の底」を掌握した時、あなたのエディタは単なるツールから、あなたの脳髄を直接拡張する「専用OS」へと進化する。既製品の設定ファイルを探してコピペするだけのエンジニアには、決して到達できない聖域へようこそ。
次に必要なのは、この「ミニマルLSPクライアント」に、あなたが普段使っている言語の特性をどうマッピングするかという、君自身の知的探究心だけだ。