Quelques astuces pour améliorer les performances de Neovim au quotidien

En fait, il s'agit plutôt de bien choisir les plugins pour Neovim ou de les configurer pour en tirer le maximum même dans les situations les plus difficiles; qu'il s'agisse de travailler avec énormément de fichiers ou sur un disque où les accès sont plutôt lents (typiquement un montage réseau).

Comme d'habitude j'emploie indifféremment Neovim et Vim (même si j'utilise principalement le premier), tout ce qui est écrit ici est valable pour les deux.

Analyse statique asynchrone

Il existe de nombreux plugins pour analyser le code (linting) et rapporter certaines erreurs directement dans l'éditeur. Cette fonctionnalité exploite généralement des outils externes (des linters, par exemple ESLint pour JavaScript, l'interpréteur php et/ou phpstan pour PHP) et un plugin Vim se charge d'intégrer le résultat dans l'éditeur.

Pendant longtemps, j'ai utilisé l'excellent syntastic. Ce plugin fonctionne très bien mais de manière synchrone, ce qui signifie que pendant l'analyse, l'éditeur est bloqué en attendant le résultat du ou des linters. En fonction de l'outil d'analyse, de la taille du fichier, des performances d'accès au système de fichier et même de la quantité d'erreurs, ce phénomène est plus ou moins gênant. C'est pourquoi, il vaut mieux utiliser un plugin gérant ce processus de manière asynchrone (ce qui est possible avec Neovim et Vim 8).

Il existe plusieurs autres plugins de ce type et j'utilise actuellement ALE (pour Asynchronous Lint Engine) qui répond parfaitement à mes besoins. Mieux, sur le même principe, ce plugin sait également corriger le code par exemple avec Prettier pour JavaScript (et bientôt PHP) ou PHP-CS-Fixer ou phpcbf de PHP_CodeSniffer pour PHP. Avec ça plus d'excuse pour ne pas respecter les coding standards du projet.

En terme de configuration de ALE, mon init.vim contient quelque chose comme:

nmap <silent> <C-k> <Plug>(ale_previous_wrap)
nmap <silent> <C-j> <Plug>(ale_next_wrap)

let g:ale_sign_column_always = 1

let g:ale_fix_on_save = 1
let g:ale_fixers = {
\   'php': ['trim_whitespace', 'php_cs_fixer'],
\   'javascript': ['trim_whitespace', 'prettier'],
\   'yaml': ['trim_whitespace'],
\   'markdown': ['trim_whitespace']
\}

" most linters are automatically detected and default works for me
let g:ale_php_phpcs_standard = 'PSR2'

Génération des tags

Vim est capable d'utiliser un ou des fichiers de tags générés avec ctags ou l'un de ces forks pour naviguer dans le code source. Tout le problème est donc de générer et de tenir à jour ce fichier.

Le plugin Gutentags réalise cette tâche de manière très efficace en permettant notamment la mise à jour incrémentale du fichier de tags là où certains plugins tentent d'indexer le projet dans sa totalité. Il est également possible de le configurer pour écrire le fichier de tags ailleurs qu'à la racine du projet par exemple sur une partition locale voire carrément montée en mémoire. Bref, tous ces atouts font de Gutentags un compagnon de choix sur de gros projets et/ou avec des accès disques un peu ralentis. Et surtout, il n'est a priori pas nécessaire d'ignorer des pans entiers du projets ou de ces dépendances.

En terme de configuration, c'est assez simple :

let g:gutentags_ctags_exclude_wildignore = 0
let g:gutentags_cache_dir = "~/.cache/gutentags"

" because Ctrl-] is close to impossible to use for me on fr MacOS keyboard
nnoremap <Leader>t :execute 'tjump' expand('<cword>')<CR>
nnoremap <Leader>wt :execute 'stjump' expand('<cword>')<CR>

Recherche de fichiers

En plus de naviguer dans le code source par symbole, il est courant de chercher un fichier par son nom et surtout par un ou des morceaux de nom (fuzzy matching). Plusieurs plugins fournissent ce type de fonctionnalités, mais j'utilise depuis de nombreuses années CtrlP.vim qui remplit cette fonction avec brio. Malgré tout, à mesure que le projet grandit (ou avec des accès disque ralentis), l'indexation et la recherche prennent de plus en plus de temps.

Pour améliorer le temps d'indexation, il est tentant d'ignorer des parties du projet par exemple les dépendances (les dossiers vendor sur un projet PHP) mais invariablement ces dossiers non indexés finissent par manquer. Au lieu de ça ou plus exactement en complément, il est possible d'utiliser une commande du système pour construire la liste des fichiers, cette commande a toutes les chances d'être plus rapide que l'indexation de base de CtrlP écrite en language de script de Vim. Il semble que the Silver Searcher (ag) soit particulièrement indiqué mais toute commande capable de lister des fichiers dans une arborescence pourra faire l'affaire (find, ack, ...). Par ailleurs, CtrlP peut-être configuré pour conserver son cache (son index) entre 2 lancements de l'éditeur (avec la configuration par défaut, F5 permet de rafraîchir le cache après avoir lancé CtrlP avec... Ctrl-P 😀), la configuration suivante apporte donc une amélioration appréciable de l'indexation :

let g:ctrlp_user_command = 'ag %s -l -U --nocolor -g ""'
" -U tells ag to ignore .gitignore but to still take .ignore into account
" to have different ignore rules for git and for CtrlP
let g:ctrlp_clear_cache_on_exit = 0

Pour améliorer le temps de recherche lorsque l'index grandit, il est possible de configurer CtrlP pour utiliser une fonction de matching spécifique. Le plugin ctrlp-py-matcher en fournit une écrite en Python. L'auteur indique l'utiliser sur un projet avec plus de 350000 fichiers ! Bref, après avoir installé le plugin, la ligne suivante permet d'utiliser cette fonction :

let g:ctrlp_match_func = { 'match': 'pymatcher#PyMatch' }

Recherche dans les fichiers

Enfin après avoir chercher dans les symboles ou par nom de fichier, il est parfois utile de tout simplement chercher dans le contenu des fichiers d'un projet. Là encore, la taille du projet et les accès disque jouent un rôle essentiel. Pour ce genre d'opération, j'utilise le plugin vim-grepper qui lui aussi permet d'intégrer les commandes du système. Là encore, the Silver Searcher (ag) est d'une grande utilité mais si comme moi vous utilisez votre éditeur dans un code source géré avec git, git grep est potentiellement encore plus rapide dans la mesure où celui-ci indexe déjà tout les fichiers du dépôt. Je configure vim-grepper avec 2 appels différents de git grep, l'un qui cherche en tenant compte du .gitignore mais aussi dans les fichiers que git ne tracke pas et l'autre dans absolument tout. Au final, ma configuration de vim-grepper ressemble à :

map <C-g> :Grepper<cr>
map <Leader><C-g> :Grepper -side<cr>

let g:grepper               = {}
let g:grepper.highlight     = 1
let g:grepper.next_tool     = '<leader>g'
let g:grepper.simple_prompt = 1
let g:grepper.dir = 'repo,cwd'
let g:grepper.side_cmd = 'botright vnew'

runtime plugin/grepper.vim
let g:grepper.git.grepprg .= ' --untracked'

let g:grepper.tools = ['git', 'git_grep_everything', 'ag']
let g:grepper.git_grep_everything = {
\   'grepprg':    'git grep -nI --untracked --no-exclude-standard',
\   'grepformat': '%f:%l:%m',
\   'escape':     '\^$.*[]',
\ }

Il y a sans doute plein d'autres astuces possibles et d'ailleurs si vous en avez en stock je serais ravi de les lire et d'ajouter des liens dans ce billet. Mais pour moi, avec tout ça, Neovim est largement utilisable et même parfaitement adapté à des projets de toute taille.