🤹

Neovim0.11用のLSP設定

に公開

本記事はvim-jp slackの#neovim-pluginsチャンネルに助けられて書きました。

Neovim0.11でlspconfigを使わなくてもLSPを設定しやすくなった

Neovimの0.11がリリースされ、LSP設定の設定方法が新しくなりました。neovim/nvim-lspconfigがなくてもかなり簡単に設定できるようになっています。

https://zenn.dev/pandanoir/articles/4736924f5ecc72

https://github.com/neovim/nvim-lspconfig

先日公開した本では、neovim/nvim-lspconfigを使わずに設定しています。

https://zenn.dev/kawarimidoll/books/6064bf6f193b51/viewer/c185e3

このようなファイル構成で、

(config)/
├ init.lua
└ lua/
    └ lsp/
        ├ init.lua # 大元のinit.luaから呼び出すファイル
        │          # ここから他のlspの設定を読み込む
        ├ lua_ls.lua # lua_ls単体の設定
        └ foo.lua # その他lspの設定ファイルが続く…

lsp/lua_ls.luaにlua-language-server用の設定を書き、

lua/lsp/lua_ls.lua
return {
  cmd = { 'lua-language-server' },
  filetypes = { 'lua' },
  settings = {
    Lua = {
      diagnostics = {
        unusedLocalExclude = { '_*' }
      }
    }
  }
}

lsp/init.luaで各lsp用の設定を読み込んでvim.lsp.config()vim.lsp.enable()で有効化し、

lua/lsp/init.lua
-- load lsp/lua_ls.lua
local lua_ls_opts = require('lsp.lua_ls')
vim.lsp.config('lua_ls', lua_ls_opts)
vim.lsp.enable('lua_ls')

それをinit.luaから呼び出す構造です。

init.lua
require('lsp')

この方法では使用するlspに応じて設定ファイルおよびその読み込みの記述が増えるので、以下のチャプターで一括読込する仕組みを提案しています。

https://zenn.dev/kawarimidoll/books/6064bf6f193b51/viewer/018161

実はlspの設定はrequireしなくて良い

設定がどのように反映されるかは以下のヘルプに書いてあります。

https://github.com/neovim/neovim/blob/c7d8812ca785864bdbdeac72efcd611bfb6e506c/runtime/doc/lsp.txt#L119-L137

When an LSP client starts, it resolves its configuration by merging from the following (in increasing priority):

  1. Configuration defined for the '*' name.
  2. Configuration from the result of merging all tables returned by lsp/<name>.lua files in 'runtimepath' for a server of name name.
  3. Configurations defined anywhere else.

(意訳)
LSPクライアント起動時に、以下の順に設定がマージされる:

  1. '*'(全体)用の設定。
  2. 'runtimepath'内のlsp/<サーバー名>.luaファイルによって返されるすべてのテーブル。
  3. それ以外の場所で定義された設定。

面白いのは上記の2で、これを使えばユーザーが手動でvim.lsp.config()で設定を読み込む必要はありません。
init.luaのあるパス(基本的には~/.config/nvim)は'runtimepath'に入っているはずなので、その直下にlsp/<name>.luaを作りましょう。
先程の例を使うとこうなります。

(config)/
├ init.lua
├ lsp/ # 移動してきた この中の設定は名前に応じて自動で読み込まれる
│   ├ lua_ls.lua # lua_ls単体の設定
│   └ foo.lua # その他lspの設定ファイルが続く…
└ lua/
    └ lsp/
        └ init.lua # 大元のinit.luaから呼び出すファイル
                   # ここから他のlspの設定を読み込む

lua/lsp/init.luaは名前を指定してvim.lsp.enable()するだけになります。

lua/lsp/init.lua
- -- load lsp/lua_ls.lua
- local lua_ls_opts = require('lsp.lua_ls')
- vim.lsp.config('lua_ls', lua_ls_opts)
  vim.lsp.enable('lua_ls')

とはいえlspconfigを使ったほうが楽

これでlspを動かせるのですが、lspの設定をすべて書いていくは正直めんどうです。オプションなどは調整が必要かもしれませんが、起動コマンド(cmd = { 'lua-language-server' })とか対象のファイルタイプ(filetypes = { 'lua' })とかは決まりきっていることがほとんどでしょう。
そこでlspconfigです。読み込めばリポジトリのトップが'runtimepath'に入ります。そしてリポジトリ直下にlsp/があり、その中に<サーバー名>.luaのファイルが用意されています。すぐ上で説明した構造と同じです。


lsp/がある

内部では以下のように各サーバーの設定をしてくれています。

https://github.com/neovim/nvim-lspconfig/blob/32b6a6449aaba11461fffbb596dd6310af79eea4/lsp/lua_ls.lua#L55-L68

これは自動で読み込まれるので、lspconfigをインストールしさえすれば、setup()などは必要ありません。
たとえば筆者は(前傾の本の通り)mini.depsを使ってプラグインを読み込んでいるので、init.luaはこうなります。

init.lua
+ add('neovim/nvim-lspconfig')
  require('lsp')

cmdfiletypesはlspconfigに任せられるので、ユーザー側の設定からこれらを除くことができます。

lua/lsp/lua_ls.lua
 return {
-  cmd = { 'lua-language-server' },
-  filetypes = { 'lua' },
   settings = {
     Lua = {
       diagnostics = {
         unusedLocalExclude = { '_*' }
       }
     }
   }
 }

lspconfigの定義している設定で十分な場合は自前の設定ファイルを用意する必要もありません。vim.lsp.enable('サーバー名')するだけです。

lspconfigの設定を上書きしたいときは注意が必要

ここでちょっと問題があります。以下のように設定してもおそらく反映されません。

lua/lsp/lua_ls.lua
 return {
+  filetypes = { 'lua.neovim' }, -- あくまで例
   settings = {
   -- 略
   }
 }

lspの設定ファイルを読み込んでいるのはどうやらこの部分です。vim.api.nvim_get_runtime_file(('lsp/%s.lua'):format(name), true)'runtimepath'内のファイルを取り出し、vim.tbl_deep_extend('force', rtp_config or {}, config)でマージしています。

https://github.com/neovim/neovim/blob/c7d8812ca785864bdbdeac72efcd611bfb6e506c/runtime/lua/vim/lsp.lua#L416-L425

したがって、~/.config/nvimとlspconfigのディレクトリの両方が'runtimepath'に入っている場合、どちらが先にくるかで結果が変わります。プラグインマネージャによって変わるかもしれませんが、筆者の手元ではlspconfigのほうが後に来ていました。つまりlspconfigのほうが有効になり、ユーザーの作った設定は上書きされます。
ユーザーの設定を有効にしたい場合は追加の手順が必要になります。

afterを使う

おそらく'runtimepath'の後ろの方に~/.config/nvim/after/が入っているので、これを使います。

(config)/
├ init.lua
├ after/
│   └ lsp/ # afterの中に移動
│      ├ lua_ls.lua # lua_ls単体の設定
│      └ foo.lua # その他lspの設定ファイルが続く…
└ lua/
    └ lsp/
        └ init.lua # 変わっていない

こうするとユーザー設定の優先度をlspconfigよりも高めることができます。

vim.lsp.config()を使う

先程のドキュメントの3を使います。

(意訳)
LSPクライアント起動時に、以下の順に設定がマージされる:

  1. '*'(全体)用の設定。
  2. 'runtimepath'内のlsp/<サーバー名>.luaファイルによって返されるすべてのテーブル。
  3. それ以外の場所で定義された設定。

vim.lsp.config()で明示的に設定を読み込めば有効になります。

有効化切り替えの設定

tsファイルを開いたとき、nodeプロジェクトなのかdenoプロジェクトなのかによって使用するlspを変えたいという問題があります。

https://zenn.dev/kawarimidoll/articles/2b57745045b225
https://zenn.dev/mochi/articles/e6b2735108157c
https://zenn.dev/vim_jp/articles/69d26e3f7b0e35
https://zenn.dev/vim_jp/articles/10b408bc0cf077

root_dirの引数の関数の実行を切り替えることで、これに対応できます。

https://github.com/neovim/neovim/blob/c7d8812ca785864bdbdeac72efcd611bfb6e506c/runtime/doc/lsp.txt#L707-L714

筆者はこんな感じに設定しました。

after/lsp/denols.lua
return {
  root_dir = function(bufnr, callback)
    local found_dirs = vim.fs.find({
      'deno.json',
      'deno.jsonc',
      'deps.ts',
    }, {
      upward = true,
      path = vim.fs.dirname(vim.fs.normalize(vim.api.nvim_buf_get_name(bufnr))),
    })
    if #found_dirs > 0 then
      return callback(vim.fs.dirname(found_dirs[1]))
    end
  end,
}

参考 https://github.com/kyoh86/dotfiles/blob/0fafb25ec68ea9027b5373fbf82f66ed5d3b5fd1/nvim/lsp/denols.lua#L27-L34

その他tips

vim.lsp.enable()はサーバー名を配列で受け取れます。

local lsp_names = {
  'lua_ls',
  'denols',
  -- 略
}

vim.lsp.enable(lsp_names)

Discussion