Langages
C’est la partie la plus intéressante de la configuration, et celle qui demande le plus d’explication : quatre plugins doivent se mettre d’accord sur ce que veut dire « supporter Python ».
Le problème
Ajouter un langage touche normalement quatre endroits : le serveur à activer, le
paquet à installer, le parser treesitter à compiler, le formateur à brancher. Quatre
fichiers, quatre conventions de nommage différentes — le paquet mason s’appelle
typescript-language-server là où le serveur lspconfig s’appelle ts_ls — et un
oubli qui ne se manifeste que trois semaines plus tard, sur un fichier qu’on
n’ouvre jamais.
La solution : une source unique
Tout est déclaré dans lua/config/languages.lua, et rien ailleurs. Une entrée
par langage, quatre champs, tous optionnels :
Le fichier expose ensuite quatre fonctions, et chaque consommateur appelle la sienne :
| Fonction | Consommée par | Pour |
|---|---|---|
servers() | lua/plugins/lsp.lua | vim.lsp.enable() |
mason_packages() | lua/plugins/lsp.lua | l’installation automatique |
parsers() | lua/plugins/treesitter.lua | la compilation des grammaires |
formatters_by_ft() | lua/plugins/format.lua | conform |
L’agrégation déduplique : prettier est déclaré par cinq langages et n’est installé
qu’une fois. Et formatters_by_ft() lève une erreur explicite si deux langages
revendiquent le même type de fichier — une collision silencieuse serait
impossible à diagnostiquer.
Ajouter un langage, c’est donc ajouter une table de quatre lignes. C’est tout l’intérêt du dispositif, et la raison pour laquelle il vaut ses cinquante lignes de code d’agrégation.
Les réglages par serveur, à part
Un second tableau, M.server_config, porte les réglages propres à chaque serveur.
Pourquoi séparé ? Parce qu’un serveur peut couvrir plusieurs langages : ts_ls sert
JS, TS et le bloc <script> d’un fichier .vue. Ses réglages n’appartiennent
donc à aucune entrée de langage en particulier.
Le piège du runtimepath
Ces réglages sont appliqués par vim.lsp.config() après le chargement de
nvim-lspconfig, et pas en déposant un fichier lsp/ts_ls.lua à la racine de la
configuration. Ce n’est pas un détail de style.
Le répertoire de configuration est en position 1 du runtimepath, nvim-lspconfig
en position 10. Neovim fusionne les fichiers lsp/ dans l’ordre du chemin, donc
celui du plugin est appliqué par-dessus le nôtre. Les tables imbriquées comme
settings survivent à cette fusion ; une liste, non. La clé filetypes de
ts_ls perdrait son entrée vue, et les fichiers .vue cesseraient
silencieusement d’être typés.
L’installation automatique
mason installe, mais il faut lui dire quoi. mason-lspconfig faisait ce pont ; il a
été remplacé par une douzaine de lignes dans lua/plugins/lsp.lua qui parcourent
mason_packages() et installent ce qui manque.
Un détail compte : le code vérifie package:is_installing() avant de lancer une
installation, parce que tree-sitter-cli peut déjà être en vol — c’est la
spécification treesitter qui le possède et l’installe au démarrage. Sans cette
vérification, Package:install() lève une assertion.
L’autre point à connaître est quand ça se produit : lspconfig charge sur
BufReadPre, donc un nvim lancé à vide n’installe aucun serveur. Il faut ouvrir
un fichier. Les parsers treesitter, eux, s’installent dès le démarrage.
Le LSP, sans framework
Depuis Neovim 0.11, l’activation d’un serveur passe par l’API native. nvim-lspconfig
n’est plus un framework : il ne fournit que les fichiers lsp/*.lua de centaines de
serveurs, que l’API native consomme.
vim.lsp.config('*', …) annonce à tous les serveurs, en une fois, ce que le moteur
de complétion sait faire. Les réglages par serveur se fusionnent par-dessus.
Un seul raccourci LSP est ajouté à la main, gd — c’est le seul que Neovim 0.12 ne
fournit pas. Il est posé dans une autocommande LspAttach avec buffer = event.buf,
donc il n’existe que dans les buffers où un serveur répond réellement. <leader>th
suit la même logique, avec en plus un test de capacité : le raccourci n’est créé que
si le serveur sait produire des inlay hints.
Ce qui ne se devine pas
Ces points ont tous coûté du temps à diagnostiquer. Ils sont commentés dans le code, et valent d’être lus avant de reproduire la configuration ailleurs.
Vue exige deux serveurs. Depuis vue_ls v3, le takeover mode n’existe plus :
ts_ls doit charger @vue/typescript-plugin et inclure vue dans ses filetypes
pour comprendre le <script> d’un .vue. Le plugin est fourni dans le paquet mason
vue-language-server, d’où le chemin en dur vers stdpath('data').
Python fait tourner deux serveurs, volontairement. basedpyright pour les types
et la complétion, ruff pour le lint et le formatage. Le survol de ruff est
désactivé via on_attach, sinon K afficherait la réponse du premier serveur à
répondre — donc au hasard.
biome et eslint ne se marchent pas dessus. Tous deux déclarent
workspace_required = true : chacun ne s’attache que dans un projet portant son
propre fichier de configuration. Les deux peuvent donc être activés sans conflit.
biome lint, prettier formate. biome reste actif comme serveur pour ses
diagnostics sur JS/TS/JSON/CSS, mais ne formate plus rien. Comme un formateur est
déclaré pour chacun de ces types, le repli lsp_format = 'fallback' de conform ne
demande jamais à biome de formater. Et biome ignore de toute façon les .scss — il
annonce « no files were processed » et laisse le fichier intact, sans erreur.
Le prettier du projet est prioritaire. conform résout la commande via
from_node_modules, donc un node_modules/.bin/prettier local et son .prettierrc
passent devant l’installation mason. Attention si le projet a un biome.json sans
configuration prettier : les deux outils peuvent diverger sur les guillemets ou les
virgules finales.
Vue et Svelte sont formatés par leur LSP. Leur serveur embarque prettier et le
plugin correspondant ; un prettier nu ne sait pas analyser ces fichiers. Ces deux
langages n’ont donc pas d’entrée formatters, et tombent sur le repli LSP.
Rust n’est pas dans servers. rustaceanvim démarre rust_analyzer lui-même.
L’activer aussi par vim.lsp.enable() attacherait deux clients à chaque buffer
Rust. rust-analyzer est bien installé par mason, mais jamais activé par cette
voie — et rustfmt vient avec la chaîne d’outils Rust, donc il est absent de la
liste mason.
shellcheck n’est pas branché à la main. bashls le détecte sur le $PATH et
remonte ses résultats comme diagnostics. Il suffit qu’il soit installé.
Neovim distingue sh, bash et zsh comme trois types de fichiers. Un
formateur déclaré pour bash ne s’applique pas à un .sh, que Neovim classe en
sh.
La complétion
blink.cmp remplace nvim-cmp et son écosystème de sources : lsp, path,
snippets et buffer sont intégrés, il n’y a donc plus un plugin par source à
maintenir en cohérence. La seule source externe est lazydev, avec un
score_offset = 100 pour qu’elle passe devant les suggestions de lua_ls sur les
chemins de require.
Le plugin est épinglé en 1.*. La raison est pratique : lazy télécharge alors le
moteur de recherche floue Rust précompilé, au lieu d’exiger un cargo build local.
Le préréglage enter conserve les habitudes de nvim-cmp — Entrée accepte,
Ctrl + e annule, Ctrl + espace ouvre, Tab circule dans les emplacements d’un
snippet.
Le formatage
conform formate à l’enregistrement, avec un timeout_ms = 500 et un repli sur le
LSP quand aucun formateur n’est déclaré pour le type de fichier. <leader>f formate
à la demande.
:ConformInfo répond à la question qui se pose toujours en premier quand le
formatage fait autre chose que prévu : quels formateurs s’appliquent à ce buffer, et
lesquels ont été trouvés sur le disque.