Développer des modules pour VIM
Depuis que je suis (re)passé sous Vim pour développer, j’ai passé un peu de temps à me peaufiner une configuration aux petits oignons. En effet, quand on a pris l’habitude de développer avec un IDE, le moindre refactoring, la moindre recherche ou les imports automatiques nous font oublier que dans la vraie vie, tout n’est pas aussi luxueux ;)
Heureusement, il existe un grand nombre de plugins pour Vim qui peuvent grandement nous simplifier la vie. Parmi ceux-ci, j’utilise :
- ack.vim est un module permettant d’utiliser Ack dans Vim.
- DirDiff.vim permet de comparer (et éventuellement merger) des arborescences de répertoires.
- fugitive est une interface à Git, dans Vim.
- NERDTree pour ajouter un explorateur de fichier à Vim.
- surround est un module qui permet de facilement jouer avec les “encadrements”.
- tagbar permet d’afficher tous les tags (classes, méthodes, variables, …) du fichier courant.
Une fois ces modules installés, il me manque trois petites choses:
- Afficher les différences entre le fichier en cours d’édition et sa dernière version sauvegardée.
- La possibilité de rechercher des fichiers.
- Pouvoir naviguer entre les fichiers d’un projet via les tags.
J’ai donc créé trois extensions permettant de répondre à ces besoins.
filediff.vim
L’idée est de pouvoir visualiser les différences entre le fichier en cour d’édition et sa dernière version sauvegardée.
Voici le code :
1 " Original ! (see :help :DiffOrig)
2 command DiffOrig vert new | set bt=nofile | r # | 0d_ | diffthis | wincmd p | diffthis
3
4 let s:diff_buffer_number = -1
5
6 function! s:diffToggle()
7 if s:diff_buffer_number == -1
8 exec 'vert new | set bt=nofile | set ma | r # | 0d_ | diffthis | set noma | wincmd p | diffthis'
9 let s:diff_buffer_number = last_buffer_nr()
10 else
11 exec s:diff_buffer_number . "bd"
12 let s:diff_buffer_number = -1
13 endif
14 endfunction
15
16 command! DiffOrigToggle call s:diffToggle()
A la ligne 2 nous créons la commande DiffOrig. Elle est directement issue de la documentation de Vim (:help :DiffOrig). Cette commande remplit presque parfaitement le besoin. Cependant, si vous l’essayez, nous verrez qu’elle présente deux défauts :
- Pour fermer la fenêtre présentant le code sauvegardé, il faudra, soit se déplacer dans la fenêtre en question (
Ctrl-w-w) et la fermer (:q), soit lister les buffers (:ls) et fermer celui correspondant à la fenêtre (:<n>bd). Il serait plus simple de pouvoir rappeler la même commande qui fermerait la fenêtre si elle est déjà ouverte. Ainsi, en mappant l’appel de la commande, nous aurions un effet de toggle. - Ensuite, la fenêtre créée (via
vert new) est éditable. Ce qui peut être dangereux.
Pour résoudre ces deux problèmes, nous allons créer la fonction s:diffToggle. Dans cette fonction nous utiliserons la variable s:diff_buffer_number afin de savoir si nous avons un différentiel affiché. Ainsi, si s:diff_buffer_number est égal à –1, alors nous allons utiliser le même enchaînement que celui proposé par :DiffOrig et nous récupèrerons le numéro du dernier buffer ouvert (via last_buffer_nr()). Si s:diff_buffer_number est différent de –1 nous fermons le buffer référencé dans s:diff_buffer_number. Pour éviter que la fenêtre contenant le code du fichier sauvegardé ne soit éditable, nous faisons une petite modification dans l’enchainement des commandes de :DiffOrig en utilisant set nomodifiable (set noma).
Vim-find
L’idée, ici, est de créer une commande permettant de rechercher des fichiers sur le même modèle que la commande find.
Le code utilisé est le suivant :
1 " Find file in current directory and edit it.
2 let g:findprg="find\\ .\\ -type\\ f\\ -name\\ "
3 let g:findprgend="-exec bash -c \"echo '{}':0:0\" \\\;"
4 function! Find(args)
5 let grepprg_bak=&grepprg
6 exec "set grepprg=" . g:findprg
7 execute "silent! grep \"" . a:args . "\" " . g:findprgend
8 botright copen
9 let &grepprg=grepprg_bak
10 exec "redraw!"
11 endfunction
12 command! -nargs=* -complete=file Find call Find(<q-args>)
Pour faire cela, nous utilisons la commande :grep en remplaçant temporairement le contenu de grepprg par la commande suivante :
Si vous essayez cette commande depuis le shell, en remplacant <args> par une expression rationnelle correspondant à une liste de fichiers, vous devriez voir s’afficher une liste de fichier sous la forme :
Nous utilisons :copen pour afficher le résultat de notre recherche dans une fenêtre ouverte en bas de le fenêtre courante (via :botright).
cscope.vim
cscope est un outil permettant de naviguer dans du code. Il permet ainsi de retrouver (par exemple) le fichier de déclaration d’une méthode, et toutes les utilisations de cette méthode.
Il est possible de compiler Vim avec le support de cscope (--enable-cscope), à la suite de quoi nous pourrons utiliser la commande :cscope pour parcourir le code. Ce qui me dérange le plus, dans ce support de cscope, est la façon dons les résultats de recherche sont affichés. En effet, ils sont présentés en dehors de Vim alors qu’une présentation similaire à celle mise en place dans le cas de la recherche de fichiers proposée ci-dessus serait beaucoup plus agréable.
Voici le code que j’ai donc mis en place pour arriver à ce résultat :
1 if &cp || exists('g:loaded_cscope')
2 finish
3 endif
4
5 if !exists('g:cscope_bin')
6 if executable('cscope')
7 let g:cscope_bin = 'cscope'
8 elseif executable('cscope.exe')
9 let g:cscope_bin = 'cscope.exe'
10 else
11 echomsg 'cscope: cscope command not found, skipping plugin'
12 finish
13 endif
14 else
15 let g:cscope_bin = expand(g:cscope_bin)
16 if !executable(g:cscope_bin)
17 echomsg 'cscope: cscope command not found in specified place,'
18 \ 'skipping plugin'
19 finish
20 endif
21 endif
22
23 let g:loaded_cscope = 1
24
25 if !exists('g:cscope_autoregen')
26 let g:cscope_autoregen=1
27 end
28
29 let g:cscope_files_exist=0
30 if !exists('g:cscope_file')
31 let g:cscope_file = ".cscope"
32 end
33 let g:cscope_reffile = g:cscope_file . ".out"
34 let g:cscope_sourcefile = g:cscope_file . ".files"
35 if filereadable(g:cscope_reffile) && filereadable(g:cscope_sourcefile)
36 let g:cscope_reffile=expand(g:cscope_reffile)
37 let g:cscope_sourcefile=expand(g:cscope_sourcefile)
38 let g:cscope_files_exist=1
39 end
40
41 function! CscopeAdd(...)
42 let remove_sourcefile=system("rm -f " . g:cscope_sourcefile)
43 for ext in a:000
44 let find_command="find . -name \"*." . ext . "\" >> " . g:cscope_sourcefile
45 let find_result=system(find_command)
46 endfor
47
48 let cscope_command=g:cscope_bin . " -b -f " . g:cscope_reffile . " -i " . g:cscope_sourcefile
49 let cscope_result=system(cscope_command)
50
51 let g:cscope_files_exist=1
52
53 echomsg "cscope reference and source files created : " . g:cscope_reffile . ", " . g:cscope_sourcefile
54 endfunction
55 command! -nargs=* CscopeAdd call CscopeAdd(<f-args>)
56
57 if exists('g:cscope_autogen')
58 call CscopeAdd(g:cscope_autogen)
59 end
60 if g:cscope_autoregen == 1 && g:cscope_files_exist == 1
61 " TODO: regenerate cscope files!
62 end
63
64 function! CscopeFind(index, symbol)
65 if g:cscope_files_exist == 0
66 echomsg "cscope: no reference and source files found. Use :CscopeAdd to generate them"
67 else
68 let cscope_command=g:cscope_bin . " -f " . g:cscope_reffile . " -i " . g:cscope_sourcefile . " -L -" . a:index . " " . a:symbol . " | awk '{ printf(\"%s|%s|\", $1, $3); for(i=4; i<NF; i++) { printf(\" %s\", $i) }; printf(\"\\n\") }'"
69 cexpr system(cscope_command)
70 botright copen
71 end
72 endfunction
73 command! -nargs=* CscopeFind call CscopeFind(<f-args>)
74 nmap <leader>a :CscopeFind 0 <c-r>=expand("<cword>")<CR><CR>
75 nmap <leader>d :CscopeFind 1 <c-r>=expand("<cword>")<CR><CR>
76 nmap <leader>t :CscopeFind 4 <c-r>=expand("<cword>")<CR><CR>
77 nmap <leader>g :CscopeFind 6 <c-r>=expand("<cword>")<CR><CR>
Pour commencer, nous vérifions que nous le sommes pas en mode compatible et que le plugin n’a pas déjà été chargé (lignes 1 à 3).
Les lignes 5 à 13 servent à rechercher la présence d’un exécutable cscope (ou cscope.exe) dans le PATH, et a stocker cette information dans la variable g:cscope_bin. Si la variable g:cscope_bin a été positionnée par l’utilisateur (dans son .vimrc, par exemple), nous vérifions bien qu’elle pointe vers un exécutable (lignes 14 à 21).
Pour fonctionner, cscope a besoin de deux fichiers :
- Un fichier de sources qui liste tous les fichiers qui doivent être analysés pas
cscope - Un ficher de références, permettant à
cscopede répondre à nos demandes
Le premier fichier peut simplement être créé avec un find. Par exemple, pour lister tous les fichiers dans un projet en C nous utiliserons la commande suivante :
find . -name "*.[c|h]" > cs.files
Pour créer le second fichier, nous utilisons cscope :
cscope -b -f cs.out -i cs.files
Ces deux fichiers étant créés, nous pouvons rechercher des références dans le projet :
cscope -f cs.out -i cs.files -L -0 Init_rosxauth
Nous devons donc faire la même chose dans notre module VIM.
Nous allons utiliser les variables g:cscope_sourcefile et g:cscope_reffile pour stocker les chemins vers les fichiers de sources et références. Ceci est mis en place lignes 29 à 39. Au passage, nous positionnons la valeur 1 dans la variable g:cscope_files_exist si ces fichiers existent déjà.
Nous mettons ensuite en place la commande CscopeAdd, associé à la fonction du même nom. Cette fonction prend en paramètre la liste des extensions de fichiers à faire analyser par cscope, et crée les fichiers de sources et de références (lignes 41 à 55).
Pour faire la recherche, nous créons la commande (et la fonction) CscopeFind qui prend en paramètre la référence recherchée et affiche les résultats trouvés (lignes 64 à 73). La fonction CscopeFind prend en paramètre le périmètre de recherche et le terme recherché. Pour simplifier ces recherches, j’ai ajouté des mappings vers certains appels de cette commande (lignes 74 à 77). Ainsi, en nous positionnant sur un terme et en tapant <leader>a nous retrouvons toutes ses définitions…