Share your feedback via email, using Mastodon or
Neovim LSP, linting, and formatting: the clean way
I’ve recently completely revisited my
Neovim config
and took the extra mile to also streamline how it manages LSP, linting, and formatters. I had enough to remind myself to maintain and sync three different files every time I want to add a new tool.
Here is how I’ve done it.
The files structure
My Neovim setup relies on:
- mason-tool-installer.nvim to easily install and manage LSP servers, DAP servers, linters, and formatters. Note that i requires mason.nvim . Make sure you’re calling them like that in your plugin manager (I’m using Lazy.nvim):
"mason-org/mason.nvim",
lazy = false,
config = true,
},
{
"WhoIsSethDaniel/mason-tool-installer.nvim",
dependencies = {
"mason-org/mason.nvim",
},
event = "VeryLazy",
config = function()
require("config.mason-tools")
vim.schedule(function()
vim.cmd("MasonToolsInstall")
end)
end,
},- nvim-lspconfig.lua nvim-lspconfig is taking care of LSP server default configurations for me
- conform.lua is my formatter plugin
All these plugins are configured with a file placed in ~/.config/nvim/lua/config/. The thing is that they all called their own tool, but some are redundant across files which makes it annoying to maintain.
To simplify that, I’ve created a tools.lua file in the same folder.
tools.lua: the core of my new organization
This file (see code below) lists all the LSPs, linters, and formatters that the aforementioned plugins require. This is a template, feel free to add any more tools to the lsit.
The remaining task was to rewrite each plugin’s configuration so they can fetch what they need. I’ve copied each code below as well. You should just have to copy/paste them.
Just note that:
- I use
blink.cmpinnvim-lspconfig.lua. Adjust that if you use something else likenvim-cmp. - The
nvim-lspconfig.luais used to collect any specific configuration you want to overwrite. If you’re satisfied with the default, you don’t have to list any tool here.
local M = {}
M.languages = {
lua = {
filetypes = { "lua" },
lsp = {
server = "lua_ls",
mason = "lua-language-server",
},
formatters = {
{ name = "stylua", mason = "stylua" },
},
linters = {
{ name = "luacheck", mason = "luacheck" },
},
},
python = {
filetypes = { "python" },
lsp = {
server = "basedpyright",
mason = "basedpyright",
},
formatters = {
{ name = "ruff_format", mason = "ruff" },
},
}
M.global = {
{ mason = "eslint_d" },
}
return Mlocal tools = require("config.tools").languages
local ensure = {}
local seen = {}
local function add(pkg)
if pkg and not seen[pkg] then
seen[pkg] = true
table.insert(ensure, pkg)
end
end
for _, lang in pairs(tools) do
if lang.lsp then
add(lang.lsp.mason)
end
for _, formatter in ipairs(lang.formatters or {}) do
add(formatter.mason)
end
for _, linter in ipairs(lang.linters or {}) do
add(linter.mason)
end
end
for _, tool in ipairs(tools.global or {}) do
add(tool.mason)
end
require("mason-tool-installer").setup({
ensure_installed = ensure,
-- run_on_start = true,
-- auto_update = false,
-- start_delay = 3000,
})local capabilities = require("blink.cmp").get_lsp_capabilities()
local base = {
capabilities = capabilities,
}
local tools = require("config.tools").languages
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
if client:supports_method() and client:supports_method("textDocument/codeLens") then
vim.lsp.codelens.enable(true)
end
end,
})
local lsps = {
lua_ls = {
settings = {
Lua = {
filetypes = { "lua" },
diagnostics = {
globals = { "vim" },
},
workspace = {
checkThirdParty = false,
},
},
telemetry = {
enable = false,
},
},
},
}
local enabled = {}
for _, lang in pairs(tools) do
if lang.lsp then
enabled[lang.lsp.server] = true
end
end
for server in pairs(enabled) do
local config = lsps[server] or {}
vim.lsp.config(server, vim.tbl_deep_extend("force", base, lsps[server] or {}))
vim.lsp.enable(server)
endlocal tools = require("config.tools").languages
local formatters_by_ft = {}
for _, lang in pairs(tools) do
for _, ft in ipairs(lang.filetypes or {}) do
formatters_by_ft[ft] = {}
for _, formatter in ipairs(lang.formatters or {}) do
table.insert(formatters_by_ft[ft], formatter.name)
end
end
end
require("conform").setup({
formatters_by_ft = formatters_by_ft,
format_on_save = {
timeout_ms = 500,
lsp_format = "fallback",
},
})Installing a new tool
The question you may have is: once I’ve listed the tools, how do I install them? And the answer is: you don’t :)
Any new tool will automatically be installed on Neovim start thanks to the execution of MasonToolsInstall (see the plugins configuration file above). Of course, if you need to activate a new tool without restarting Neovim, you can simply call :MasonToolsInstall yourself.
Is it perfect?
I’m not a dev and don’t have any formal IT training, but this is what works for me. It streamlines my setup and makes it way easier to maintain and update. If you have a better approach or any tips for improvement, definitely let me know, I’m all ears.