Neovim0.11用のLSP設定
本記事はvim-jp slackの#neovim-pluginsチャンネルに助けられて書きました。
Neovim0.11でlspconfigを使わなくてもLSPを設定しやすくなった
Neovimの0.11がリリースされ、LSP設定の設定方法が新しくなりました。neovim/nvim-lspconfigがなくてもかなり簡単に設定できるようになっています。
先日公開した本では、neovim/nvim-lspconfigを使わずに設定しています。
このようなファイル構成で、
(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用の設定を書き、
return {
  cmd = { 'lua-language-server' },
  filetypes = { 'lua' },
  settings = {
    Lua = {
      diagnostics = {
        unusedLocalExclude = { '_*' }
      }
    }
  }
}
lsp/init.luaで各lsp用の設定を読み込んでvim.lsp.config()とvim.lsp.enable()で有効化し、
-- 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から呼び出す構造です。
require('lsp')
この方法では使用するlspに応じて設定ファイルおよびその読み込みの記述が増えるので、以下のチャプターで一括読込する仕組みを提案しています。
実はlspの設定はrequireしなくて良い
設定がどのように反映されるかは以下のヘルプに書いてあります。
When an LSP client starts, it resolves its configuration by merging from the following (in increasing priority):
- Configuration defined for the
'*'name.- Configuration from the result of merging all tables returned by
lsp/<name>.luafiles in 'runtimepath' for a server of namename.- Configurations defined anywhere else.
(意訳)
LSPクライアント起動時に、以下の順に設定がマージされる:
'*'(全体)用の設定。
'runtimepath'内のlsp/<サーバー名>.luaファイルによって返されるすべてのテーブル。- それ以外の場所で定義された設定。
面白いのは上記の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()するだけになります。
- -- 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/がある
内部では以下のように各サーバーの設定をしてくれています。
これは自動で読み込まれるので、lspconfigをインストールしさえすれば、setup()などは必要ありません。
たとえば筆者は(前傾の本の通り)mini.depsを使ってプラグインを読み込んでいるので、init.luaはこうなります。
+ add('neovim/nvim-lspconfig')
  require('lsp')
cmdとfiletypesはlspconfigに任せられるので、ユーザー側の設定からこれらを除くことができます。
 return {
-  cmd = { 'lua-language-server' },
-  filetypes = { 'lua' },
   settings = {
     Lua = {
       diagnostics = {
         unusedLocalExclude = { '_*' }
       }
     }
   }
 }
lspconfigの定義している設定で十分な場合は自前の設定ファイルを用意する必要もありません。vim.lsp.enable('サーバー名')するだけです。
lspconfigの設定を上書きしたいときは注意が必要
ここでちょっと問題があります。以下のように設定してもおそらく反映されません。
 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)でマージしています。
したがって、~/.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クライアント起動時に、以下の順に設定がマージされる:
'*'(全体)用の設定。
'runtimepath'内のlsp/<サーバー名>.luaファイルによって返されるすべてのテーブル。- それ以外の場所で定義された設定。
vim.lsp.config()で明示的に設定を読み込めば有効になります。
有効化切り替えの設定
tsファイルを開いたとき、nodeプロジェクトなのかdenoプロジェクトなのかによって使用するlspを変えたいという問題があります。
root_dirの引数の関数の実行を切り替えることで、これに対応できます。
筆者はこんな感じに設定しました。
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,
}
その他tips
vim.lsp.enable()はサーバー名を配列で受け取れます。
local lsp_names = {
  'lua_ls',
  'denols',
  -- 略
}
vim.lsp.enable(lsp_names)

Discussion