LSP natif dans Neovim 0.11 : zéro plugin, zéro compromis
- Publié le
- ·7 min de lecture
Ce qui a changé avec Neovim 0.11
Pendant des années - genre, longtemps - configurer le LSP dans Neovim c'était synonyme de nvim-lspconfig. Ce plugin faisait la job : configs par défaut pour des centaines de serveurs, gestion du cycle de vie, tout ça. C'était du solide. Mais c'était aussi une dépendance de plus, avec sa propre logique, ses propres couches d'abstraction. Un truc de plus à maintenir.
Neovim 0.11 a vraiment changé le game.
Le client LSP intégré a reçu deux nouvelles fonctions. Juste deux, mais ça change tout :
vim.lsp.config(): tu déclares la config d'un serveur LSP directement, dans Neovim mêmevim.lsp.enable(): tu actives les serveurs pour tel ou tel filetype
C'est un changement philo autant que technique. Neovim dit maintenant "le LSP, c'est nous qui le gérons, c'est pas un plugin". C'est une shift importante.
L'architecture de ma configuration LSP
Toute ma config LSP vit dans un fichier : lua/plugins/lsp.lua. Fini les trois dossiers, les 15 fichiers d'imports. Juste un. Et dedans, tu géres trois affaires : installer les serveurs avec Mason, les configurer nativement, et les keybindings LSP.
Mason : le gestionnaire de serveurs
Mason, c'est ton package manager pour les serveurs LSP, les linters, les formateurs. C'est pas pip ou npm. C'est spécialisé pour l'outillage de dev dans Neovim.
{
"williamboman/mason.nvim",
cmd = "Mason",
opts = {
ui = {
border = "rounded",
icons = {
package_installed = "✓",
package_pending = "➜",
package_uninstalled = "✗",
},
},
},
}
Pour l'auto-installation, j'utilise mason-tool-installer qui garantit que tous les serveurs sont présents au démarrage :
{
"WhoIsSethDaniel/mason-tool-installer.nvim",
dependencies = { "mason.nvim" },
opts = {
ensure_installed = {
-- LSP servers
"lua-language-server",
"typescript-language-server",
"pyright",
"html-lsp",
"css-lsp",
"json-lsp",
"yaml-language-server",
"tailwindcss-language-server",
"emmet-language-server",
-- Formatters
"stylua",
"prettier",
"black",
"isort",
},
},
}
La commande :Mason ouvre une interface graphique avec bordures arrondies et icônes pour gérer visuellement les installations.
Configuration native des 9 serveurs
Là, le vrai truc. C'est direct. Chaque serveur se déclare avec vim.lsp.config(), puis tu les actives tous d'un coup avec vim.lsp.enable() :
-- Capabilities enrichies par blink.cmp (autocomplétion)
local capabilities = require("blink.cmp").get_lsp_capabilities()
-- lua_ls : Lua avec LuaJIT
vim.lsp.config("lua_ls", {
capabilities = capabilities,
settings = {
Lua = {
runtime = { version = "LuaJIT" },
diagnostics = { globals = { "vim" } },
workspace = { checkThirdParty = false },
telemetry = { enable = false },
},
},
})
-- ts_ls : TypeScript / JavaScript
vim.lsp.config("ts_ls", {
capabilities = capabilities,
})
-- pyright : Python avec typage
vim.lsp.config("pyright", {
capabilities = capabilities,
})
-- Serveurs web
vim.lsp.config("html", { capabilities = capabilities })
vim.lsp.config("cssls", { capabilities = capabilities })
vim.lsp.config("jsonls", { capabilities = capabilities })
vim.lsp.config("yamlls", { capabilities = capabilities })
vim.lsp.config("tailwindcss", { capabilities = capabilities })
vim.lsp.config("emmet_language_server", { capabilities = capabilities })
-- Activation de tous les serveurs
vim.lsp.enable({
"lua_ls",
"ts_ls",
"pyright",
"html",
"cssls",
"jsonls",
"yamlls",
"tailwindcss",
"emmet_language_server",
})
Quelques trucs sur la config lua_ls qui valent la peine d'expliquer :
runtime.version = "LuaJIT": parce qu'on code pour Neovim qui utilise LuaJIT, pas le Lua vanilla 5.1diagnostics.globals = { "vim" }: sinon lua_ls va te crier dessus à chaque ligne avec "variable vim not found". Lourd.workspace.checkThirdParty = false: tue ce popup chieur qui demande si tu veux charger des types tiers à chaque fois que tu ouvres le projet
C'est plus propre que lspconfig. Pas de table servers à wrapper, pas de boucle for, pas d'abstraction intermédiaire qui te cache ce qui se passe. Tout est explicite.
Les diagnostics
Les diagnostics (erreurs, warnings, hints) sont configurés globalement avec vim.diagnostic.config(). J'utilise des icônes personnalisées définies dans un fichier config/icons.lua centralisé :
local icons = require("config.icons")
vim.diagnostic.config({
signs = {
text = {
[vim.diagnostic.severity.ERROR] = icons.diagnostics.Error,
[vim.diagnostic.severity.WARN] = icons.diagnostics.Warn,
[vim.diagnostic.severity.HINT] = icons.diagnostics.Hint,
[vim.diagnostic.severity.INFO] = icons.diagnostics.Info,
},
},
virtual_text = {
spacing = 4,
prefix = "■",
},
severity_sort = true,
float = {
border = "rounded",
source = true,
},
})
Le severity_sort = true est important : les erreurs apparaissent toujours avant les warnings dans la signcolumn. Le prefix = "■" donne un marqueur visuel discret mais visible pour le texte virtuel des diagnostics inline.
Les keybindings LSP
Les raccourcis sont attachés via l'autocmd LspAttach, ce qui garantit qu'ils ne sont disponibles que dans les buffers où un serveur LSP est actif :
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("lsp-attach", { clear = true }),
callback = function(event)
local map = function(keys, func, desc, mode)
mode = mode or "n"
vim.keymap.set(mode, keys, func, { buffer = event.buf, desc = "LSP: " .. desc })
end
local telescope = require("telescope.builtin")
-- Navigation
map("gd", telescope.lsp_definitions, "Go to definition")
map("gr", telescope.lsp_references, "Go to references")
map("gI", telescope.lsp_implementations, "Go to implementation")
map("gD", vim.lsp.buf.declaration, "Go to declaration")
-- Informations
map("K", vim.lsp.buf.hover, "Hover documentation")
map("<C-k>", vim.lsp.buf.signature_help, "Signature help", "i")
-- Symboles
map("<leader>ds", telescope.lsp_document_symbols, "Document symbols")
map("<leader>ws", telescope.lsp_dynamic_workspace_symbols, "Workspace symbols")
map("<leader>D", telescope.lsp_type_definitions, "Type definition")
-- Actions
map("<leader>rn", vim.lsp.buf.rename, "Rename symbol")
map("<leader>ca", vim.lsp.buf.code_action, "Code action")
end,
})
Les commandes qu'on utilise vraiment tous les jours :
gd(go to definition) : celle qu'on tapote des centaines de fois. Via Telescope, tu vois un preview du fichier destination. Et si y a plusieurs définitions, tu choisis. Magie.gr(references) : te montre tous les endroits où un symbole est appelé. Essential avant de refactor quelque chose.K(hover) : affiche la doc du symbole sous le curseur dans un floating window. Types, signatures, JSDoc. Tout. C'est gratuit, grâce au serveur LSP.<leader>rn(rename) : renomme un symbole partout dans le projet de façon sémantique. Le LSP comprend le code, il ne fait pas juste du find-and-replace bête.<leader>ca(code action) : des fix automatiques, imports manquants, refactorings que le serveur te propose. Faut juste appuyer.
Trouble.nvim pour les diagnostics
En complément du LSP natif, Trouble.nvim offre un panneau dédié aux diagnostics. Le raccourci <leader>xx ouvre une liste structurée de toutes les erreurs et warnings du projet :
{
"folke/trouble.nvim",
cmd = "Trouble",
keys = {
{ "<leader>xx", "<cmd>Trouble diagnostics toggle<cr>", desc = "Diagnostics (Trouble)" },
{ "<leader>xX", "<cmd>Trouble diagnostics toggle filter.buf=0<cr>", desc = "Buffer diagnostics" },
},
opts = {
use_diagnostic_signs = true,
},
}
C'est particulièrement utile sur un projet avec beaucoup de fichiers : au lieu de naviguer fichier par fichier pour trouver les erreurs, Trouble les agrège dans une vue unique, triée par sévérité.
Pourquoi ne pas utiliser lspconfig ?
C'est une bonne question. Si lspconfig marche bien, pourquoi tu le vires ?
- Moins de dépendances : un plugin de moins. Un truc de moins à update quand ça break.
- Forward-compatible : tu utilises l'API officielle de Neovim, pas une abstraction tierce. Neovim évolue ? Ton code évolue naturellement.
- Explicite : chaque serveur est déclaré clairement, sans magie noire. Tu sais exactement ce qui se passe.
- Simple : tu ignores comment lspconfig résout les noms de serveurs, merge les configs. Parce que tu l'utilises pas.
Ouais, pour 9 serveurs, la différence de lignes est minime. Genre 15-20 lignes. Mais la clarté gagnée ? Ça, ça compense.
Conclusion
Le LSP natif dans Neovim 0.11 c'est du sérieux. Tu as un environnement complet - go-to-definition, autocomplétion, diagnostics, rename, code actions - avec juste Mason pour installer les serveurs et quelques appels à l'API native. Plus simple, plus transparent. Et si tu sais ce que tu fais (ou même si tu sais pas), tu comprends chaque morceau de ton setup parce que c'est pas caché derrière une couche d'abstraction.
Article précédent
← Sécuriser SSH avec des algorithmes post-quantiquesArticle suivant
Pare-feu et Fail2ban : verrouiller les accès réseau du NAS→