今回は Neovim のバージョンを 0.11.1 に上げたのに際して、LSP 周りの設定をアップデートしたので、そのメモと紹介です。
0.10 からの移行の記事は多かったのですが、ゼロから始めるようなシンプルな例はあまりなかったので、私の設定を紹介しておきます。
より高度な設定については他の方の記事を参考にしてください。
エラー表示・補完・フォーマットなどは Neovim で開発するうえで ほぼ必須なわりに面倒だった 印象がありましたが、 かなりシンプルに設定できるようになったと思います。(もとの設定が汚かっただけかもしれませんが…)
以前の設定について
私の場合は、lspconfig
, mason
, none-ls (null-ls)
, blink
を使ってエラーの表示・補完・フォーマットを行っていました。
以下がプラグインの一覧です。
- neovim/nvim-lspconfig
- nvimtools/none-ls.nvim
- williamboman/mason.nvim
- williamboman/mason-lspconfig.nvim
- jay-babu/mason-null-ls.nvim
- blink.cmp
実際にどのような設定になっていたかは割愛します。
汚くて恥ずかしいので見せられません。(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-server
と stylua
を実行できるようにすること です。
そのため、それぞれの方法で適当にインストールしてもらっても大丈夫です。
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_required
を true
にすることで、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,
},
positionEncodings
、offset_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,
},
}
}
biome
と eslint_d
は require_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 周りはなかなかキレイな設定ができなくてモヤモヤしていたので、今回キレイにできてよかったです 😊