【Neovim 0.11.1】lspconfig x mise のお手軽 LSP 設定とWebフロントエンド開発者向けの設定

今回は Neovim のバージョンを 0.11.1 に上げたのに際して、LSP 周りの設定をアップデートしたので、そのメモと紹介です。

0.10 からの移行の記事は多かったのですが、ゼロから始めるようなシンプルな例はあまりなかったので、私の設定を紹介しておきます。
より高度な設定については他の方の記事を参考にしてください。

エラー表示・補完・フォーマットなどは Neovim で開発するうえで ほぼ必須なわりに面倒だった 印象がありましたが、 かなりシンプルに設定できるようになったと思います。(もとの設定が汚かっただけかもしれませんが…)

以前の設定について

私の場合は、lspconfig, mason, none-ls (null-ls), blink を使ってエラーの表示・補完・フォーマットを行っていました。

以下がプラグインの一覧です。

実際にどのような設定になっていたかは割愛します。 汚くて恥ずかしいので見せられません。(setup_handlers とか on_attach とかがごちゃごちゃしてました…)

v0.11 以降の構成

わかりやすいように Lua のみの最低限の設定を紹介します。 プラグインの管理には Lazy.nvim を使います。

Lua ファイルで、エラーの表示・補完・フォーマットができるようにした結果の動画と、その下に構成のコードを載せます。

~/.config/mise/config.toml
[tools]
lua-language-server = "3.14.0"
stylua = "2.1.0"
~/.config/nvim/init.lua
-- Lazy.nvim のインストールは省略
-- https://lazy.folke.io/installation
-- エラーなどの表示
vim.diagnostic.config({ virtual_text = true })
require("lazy").setup({
defaults = {
lazy = true,
},
spec = {
-- 動画だと tokyonight テーマを使っています。
-- { "folke/tokyonight.nvim", ... }
{
"neovim/nvim-lspconfig",
lazy = false,
init = function()
vim.lsp.enable({
"lua_ls",
})
end,
},
{
"stevearc/conform.nvim",
init = function()
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*",
callback = function(args)
require("conform").format({ bufnr = args.buf })
end,
})
end,
--- @type conform.setupOpts
opts = {
formatters_by_ft = {
lua = { "stylua" },
},
format_on_save = {
timeout_ms = 500,
lsp_format = "fallback",
},
},
},
{
"saghen/blink.cmp",
version = "*",
event = { "InsertEnter" },
opts = {},
},
},
})

解説・推しポイント

シンプルな設定でしっかり動いていて、自分で書いておきながら個人的には感動しました。
少ない記述でプラグイン同士の依存関係もなく、めちゃくちゃスッキリしてます。

それぞれのプラグインやツールがどのような役割を果たしているのか、解説していきます。

mise

mise 自体は別に Neovim となんら関係はないですが、普段使いしているのもあって mason の代わりに使うようにしました。

ここでの 目的は lua-language-serverstylua を実行できるようにすること です。 そのため、それぞれの方法で適当にインストールしてもらっても大丈夫です。

mise を使うことのメリットとして、npm のパッケージをグローバルにインストールして管理できることが挙げられます。 特に Language Server は npm で提供されているものも多いので、そういった点で mise を使うと便利です。

私は普段フロントエンドの開発をしているので、特に恩恵があります。

nvim-lspconfig

以前から lspconfig は LSP の設定をしやすくしてくれるプラグインとして大抵の人が使っていたと思います。
0.11 からは nvim 単体でもセットアップが簡単になったため、lspconfig は “Language Server の一般的な設定を提供してくれるプラグイン” になったと解釈しています。(もともと、中で何をやってくれていたのか知らなかったので、もしかしたら違うかもしれませんが…)

特に lspconfig の関数を呼び出す必要はなく、プラグインを読み込むだけで nvim-lspconfig/lsp/ にある設定が自動的に読み込まれます。

そのため、lua-language-server のように lspconfig が対応している Language Server であれば、 使用するもののみを vim.lsp.enable({}) で有効化するだけで、Language Server が起動して使えるようになります。

lspconfig を使わなくとも ~/.config/nvim/lsp に設定を自分で書くこともできますが、面倒なので頼ってしまうのが楽です。

NOTE

lspconfig の設定を上書きしたい場合は、~/.config/nvim/after/lsp/xxx.lua にそれぞれの設定を置くことで上書きできます。

conform.nvim

conform はフォーマットに特化したプラグインです。

フォーマットの仕組みと、conform で使える formatter の設定が提供されています。 formatter の一覧は conform.nvim/formatters にあります。

以前は none-ls を利用していましたが、ほとんどフォーマット用途にしか使っていなかったため、試しに乗り換えてみました。 シンプルで設定も簡単なので、フォーマットだけを目的に使うならこちらの方が良いと思います

Language Server 側でフォーマット機能が提供されているものもありますが、そうでないものも多いのでフォーマット用のプラグインを使うのが良いと思います。 Lua についても lua-language-server がフォーマット機能を提供していますが、stylua の方が(少なくとも Neovim 界隈では)人気だと思います。

blink.cmp

こちらは補完用のプラグインです。(続投)

nvim-cmp と好みの方を使ってもらえれば大丈夫だと思います。 デフォルトの設定で、LSP の情報を利用した補完ができるようになります。


このように、少ない設定で Lua での開発が快適にできるようになりました。 他の Language Server やフォーマッターを追加する際には、次のように設定を追加するだけで使えるようになります。

~/.config/mise/config.toml
[tools]
lua-language-server = "3.14.0"
stylua = "2.1.0"
"npm:@biomejs/biome" = "2.0.0-beta.5"
"npm:typescript-language-server" = "4.3.4"
# ~/.config/nvim/init.lua
require("lazy").setup({
...
spec = {
{
"neovim/nvim-lspconfig",
lazy = false,
init = function()
vim.lsp.enable({
"lua_ls",
"ts_ls",
"biome"
})
end,
},
{
"stevearc/conform.nvim",
...
opts = {
formatters_by_ft = {
lua = { "stylua" },
typescript = { "biome" },
},
format_on_save = {
timeout_ms = 500,
lsp_format = "fallback",
},
},
},
...
})

フロントエンド開発者向けの設定

少し前は TypeScript/Deno が悩みの種でしたが、最近では Prettier/ESLint/Biome も設定が面倒なところです。

以下の方針で設定をしていきます。

  • TypeScript/Deno については、deno.json などがある場合のみ Deno の Language Server を使うように。
  • Prettier/ESLint/Biome についても、biome.json などがある場合のみ Biome の Language Server / フォーマッターを使うように。

まずは Deno の設定からやりましょう。こちらは比較的シンプルです。

workspace_requiredtrue にすることで、deno.json(c) が見つからない場合は無効になります。

~/.config/nvim/after/lsp/denols.lua
--- @type vim.lsp.Config
return {
root_markers = { 'deno.json', 'deno.jsonc' },
workspace_required = true,
}

続いて、TypeScript の設定です。

Deno が無効の場合に有効になるように、root_dir をカスタマイズします。

NOTE

ts_ls にも workspace_required を設定することでより短く設定できます。 その辺の適当な ts ファイルを開いた場合に ts_ls が起動するように、やや丁寧な設定にしています。

~/.config/nvim/after/lsp/ts_ls.lua
--- @type vim.lsp.Config
return {
init_options = {
tsserver = {
path = vim.fn.exepath("tsserver"), -- ないと動かないケースがあったけど、この指定で正しいのか不明
},
},
root_dir = function(bufnr, on_dir)
-- deno 関連のファイルがある場合は、ts_ls を起動しない
local deno_files = {
"deno.json",
"deno.jsonc",
}
local deno_root = vim.fs.root(bufnr, deno_files)
if deno_root ~= nil then
return
end
local root = vim.fs.root(bufnr, {
"tsconfig.json",
"jsconfig.json",
"package.json",
".git",
})
if root then
on_dir(root)
end
end,
on_attach = function(client, bufnr)
require("twoslash-queries").attach(client, bufnr)
end,
}

続いて、Prettier/ESLint/Biome の設定です。

ESLint と Biome は LSP の方の設定もします。
併用する場合は、注意が必要です。

{
"neovim/nvim-lspconfig",
lazy = false,
init = function()
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities.general = {
positionEncodings = { "utf-16" },
}
vim.lsp.config("*", {
offset_encoding = "utf-16",
capabilities = capabilities,
})
vim.lsp.enable({
"lua_ls",
"ts_ls",
"eslint",
"biome"
})
end,
},

positionEncodingsoffset_encoding の設定をしないと、警告が出て私の場合は Biome が動きませんでした。

vim.lsp: Position Encodings ~
- ⚠️ WARNING Found buffers attached to multiple clients with different position encodings.
- Buffer 4: UTF-8 (client id(s): 2), UTF-16 (client id(s): 1, 3, 4, 5)
- ADVICE:
- Use the positionEncodings client capability to ensure all clients use the same position encoding

続いて以下のように conform の設定を追加します。

--- @type conform.setupOpts
{
formatters_by_ft = {
typescript = { "biome", "prettierd", "eslint_d" },
typescriptreact = { "biome", "prettierd", "eslint_d" },
markdown = { "prettierd" },
},
format_on_save = { ... },
formatters = {
biome = {
require_cwd = true,
},
eslint_d = {
require_cwd = true,
},
prettierd = {
-- biome が有効な場合は prettierd を無効化する
condition = function(_, ctx)
local biome_available = require("conform").get_formatter_info("biome").available
local formatters = require("conform").list_formatters_for_buffer(ctx.buf)
return not (biome_available and vim.tbl_contains(formatters, "biome"))
end,
},
}
}

biomeeslint_drequire_cwd = true を設定することで、それぞれ biome.json.eslintrc.json があるディレクトリでのみ有効になります。

prettierd の条件は少し複雑です。 biome の対象のファイルタイプで、かつ biome.json が存在する場合には prettierd を無効化するようにしています。

biome の対象のファイルタイプかどうかを確認することで、biome.json が存在する場合でも、 Markdown ファイルなどの biome の対象外のファイルタイプでは prettierd でフォーマットを行うことができます。

NOTE

formatters_by_ft では stop_after_first というオプションを設定することで、2つ以上のフォーマッターのうち有効なものを1つだけ実行することができます。 しかしながら、typescript などでは、prettier x eslint, biome のみ, biome x eslint のように複数のツールでフォーマットを行うことがあるため、stop_after_first では対応できません。

おわりに

まだ設定を大きく変えたばかりなので、使い勝手などはこれから調整していく必要があります。

後半で紹介した設定についても、biome x eslint のようなケースでうまくいくかは試していません。 今後実際に開発を通して触っていきながら、調整していきたいと思います。

いずれにしても、LSP 周りはなかなかキレイな設定ができなくてモヤモヤしていたので、今回キレイにできてよかったです 😊