Un plugin Vim à la mimine
Dans l’article précédent, intitulé une assez bonne intimité, je vous présentais GPG et le chiffrement de courriels. Nous avons alors remarqué que le contenu d’un courriel était encodé de sorte que le texte, bien que parfaitement lisible pour un anglophone, devienne totalement illisible pour un francophone du fait de l’utilisation de diacritiques dans la langue de Molière. Cette semaine, je vous propose ainsi de nous essayer à la création d’un petit plugin Vim pour décoder un bloc de texte Quoted-Printable !
L’idée étant de passer aisément de ceci :
D=C3=A8s No=C3=ABl o=C3=B9 un z=C3=A9phyr ha=C3=AF me v=C3=AAt de gla=C3=A7=
ons w=C3=BCrmiens je d=C3=AEne d=E2=80=99exquis r=C3=B4tis de b=C5=93uf au =
kir =C3=A0 l=E2=80=99a=C3=BF d=E2=80=99=C3=A2ge m=C3=BBr & c=C3=A6tera=C2=
=A0!
À cela :
Dès Noël où un zéphyr haï me vêt de glaçons würmiens je dîne d’exquis rôtis de
bœuf au kir à l’aÿ d’âge mûr & cætera !
Écrire un plugin pour Vim, de prime abord, ça peut faire peur. Mais finalement, il n’y a rien de sorcier ! Il suffit de connaître un peu la structure de fichiers de Vim, d’avoir quelques notions quant au fonctionnement de notre éditeur favori, et de ne pas se laisser impressionner par un langage de programmation quelque peu exotique : j’ai nommé VimL.
C’est une bonne situation ça, script ?
Dans sa forme la plus basique, un plugin Vim n’est rien d’autre qu’un script
.vim
déposé dans un dossier plugin/
. Celui-ci sera chargé automatiquement au
démarrage de l’éditeur. Et des plugins pour Vim,
il en existe une tripotée !
En creusant un peu, on distingue deux sortes de plugins :
- les plugins globaux, chargés quel que soit le type de fichier édité ;
- et les filetype plugins, destinés à un type de fichier bien précis.
Notre objectif étant de nous mettre à disposition une nouvelle commande
permettant de décoder un bloc de texte formaté en Quoted-Printable au sein
d’un fichier .asc
, nous allons donc nous orienter vers ce second type de
plugin.
Pour ce faire, rien de plus simple, nous allons tout simplement créer un fichier
nommé asc_qp.vim
dans le dossier ~/.vim/ftplugin/
. Notez le nom du dossier,
ftplugin
. Vous l’aurez compris, s’il s’était agi d’un plugin global, le
dossier de destination aurait été ~/.vim/plugin/
, tout simplement. Si vous
êtes utilisateur de NeoVim, la
documentation préconise de déposer votre script dans le dossier
~/.local/share/nvim/site/ftplugin/
. Dans tous les cas, si le dossier n’existe
pas, créez-le.
Le nom de notre fichier a son importance ici ! Il doit être nommé d’après le
type de fichier ciblé (dans notre cas, les fichiers .asc
) et peut être suffixé
au besoin pour éviter un conflit avec un autre plugin. On aurait donc pu
l’appeler asc.vim
, mais par sécurité j’ai opté pour le suffixer d’un _qp
pour Quoted-Printable.
Au moindre doute, n’oubliez pas : l’aide de Vim est votre meilleure amie !
:help plugin
VimL
Un plugin Vim a pour but de nous mettre à disposition de nouvelles commandes. Pour cela, on va avoir la possibilité de déclarer des fonctions et des variables auxquelles ces commandes feront appel. Rien d’extraordinaire somme toute.
Plutôt que de faire un tour exhaustif du langage et de ses rudiments, je vous propose d’en découvrir quelques aspects, en l’occurrence, ceux qui nous seront utiles pour atteindre notre objectif.
Commande
Procédons par étapes. Voyons tout d’abord comment déclarer une nouvelle commande qui sera accessible dans notre éditeur.
command DecodeQP call s:qp()
Ici nous déclarons une nouvelle commande DecodeQP
qui, quand on en fera usage,
fera appel à la fonction s:qp()
. À l’instar d’une commande builtin, nous
pourrons l’appeler comme ceci :
:DecodeQP
Il est à noter que les commandes que nous créons se doivent de commencer par une majuscule de façon à ce qu’elles n’entrent pas en conflit avec une commande prédéfinie.
Fonction
Attardons-nous un peu sur cette intrigante fonction s:qp()
. Vous l’aurez
deviné, il va nous falloir l’implémenter. Mais avant cela, demandons-nous quelle
est la signification du préfixe s:
? Il s’agit là d’une singularité du
langage. Les fonctions et les variables déclarées sont préfixées par leur
portée. Ici s:
signifie que la fonction ne sera accessible qu’au sein du
script où elle est déclarée. Les autres préfixes sont les suivants :
(aucun) Dans une fonction: local à la fonction; sinon: global
buffer-variable b: Local au buffer courant.
window-variable w: Local à la fenêtre courante.
tabpage-variable t: Local à l'onglet courant.
global-variable g: Global.
local-variable l: Local à la fonction.
script-variable s: Local au script.
function-argument a: Argument de fonction (à l'intérieur d'une fonction).
vim-variable v: Global, prédéfini par Vim.
Cela étant dit, à quoi ressemble notre fameuse fonction ?
function s:qp() abort
let l:decodeqp_command = 'perl -p -e ''s/=\n//m;s/=([\dA-F]{2})/pack H2,$1/gie'''
execute "%!" . l:decodeqp_command
endfunction
Autre particularité du langage, nous nous apercevons de l’usage du mot-clé
abort
à la déclaration de la fonction. Celui-ci permet de préciser un
comportement attendu ; ici, en l’occurrence, nous demandons à ce que la fonction
prenne fin aussitôt une erreur survenue.
D’autres mots-clés tels que dict
, closure
ou encore range
peuvent être
utilisés en signature de fonction. Voyons justement comment tirer parti de ce
dernier !
Appliquer sur un intervalle
Vim nous permet d’appeler une commande sur un intervalle (range
en anglais),
qu’il s’agisse d’un intervalle arbitraire ou d’une sélection.
Si, par exemple, nous souhaitons appliquer notre commande sur les lignes 4 à 7, il nous suffit de l’appeler comme suit :
:4,7DecodeQP
Lors de l’appel d’une commande en mode visuel, les bornes de la sélection sont
représentées par les marques '<
et '>
. Ainsi, l’appel de notre commande sur
une sélection prendra cette forme :
:'<,'>DecodeQP
Ainsi, en ajoutant le mot-clé range
à la signature de notre fonction, deux
arguments sont automatiquement mis à disposition de celle-ci : a:firstline
et
a:lastline
. Voyons comment adapter notre fonction pour en tirer avantage :
function s:qp() range abort
let l:decodeqp_command = 'perl -p -e ''s/=\n//m;s/=([\dA-F]{2})/pack H2,$1/gie'''
execute a:firstline . "," . a:lastline . "!" . l:decodeqp_command
endfunction
Il nous faudra aussi adapter notre commande pour préciser qu’elle accepte un intervalle.
command -range=% DecodeQP <line1>,<line2>call s:qp()
On définit par la même occasion une valeur par défaut dans le cas où un
intervalle n’est pas précisé ; cette valeur (%
) signifie que notre commande
s’appliquera sur l’ensemble du fichier, de la première à la dernière ligne.
Une commande qui vous ressemble
Voyons à présent comment assouplir notre plugin. Peut-être aurez vous envie d’utiliser une autre approche pour décoder un texte Quoted-Printable ; un programme dédié comme qprint plutôt qu’une regexp Perl, par exemple. Qu’à cela ne tienne ! Donnons à l’utilisateur la possibilité de définir une commande personnalisée à exécuter.
if !exists("g:decodeqp_command")
let g:decodeqp_command = 'perl -p -e ''s/=\n//m;s/=([\dA-F]{2})/pack H2,$1/gie'''
endif
function s:qp() range abort
execute a:firstline . "," . a:lastline . "!" . g:decodeqp_command
endfunction
Voyez la portée de notre variable decodeqp_command
s’étendre pour devenir
globale. Ainsi, si la variable g:decodeqp_command
n’est pas définie par
l’utilisateur dans son fichier de configuration .vimrc
, on la déclare en début
de script.
Mappings
Peut-on faire mieux ? Ne trouvez-vous pas ça usant de devoir saisir le nom de la commande au complet, 8 caractères, dont 3 majuscules… Ne pourrait-on pas disposer d’un petit raccourci clavier ?
C’est précisément là qu’entrent en jeu les mappings. Ils permettent de faire correspondre une commande avec une combinaison de touches. Vim étant un éditeur modal, comme on a déjà eu l’occasion de le découvrir dans un précédent billet, les mappings peuvent être déclinés pour chacun des modes.
Disons que nous souhaitons utiliser la séquence gcp
en mode normal pour faire
appel à notre méthode. Ainsi nous déclarerions notre mapping comme ceci :
nmap gcp :DecodeQP<CR>
De même, pour utiliser cette même séquence sur une sélection visuelle, nous
ferions usage de xmap
en lieu et place de nmap
. Notez qu’il faut ajouter un
retour chariot (<CR>
) pour s’assurer de l’exécution de notre commande, faute
de quoi elle ne ferait que s’afficher sur la ligne de commande.
Mais s’agissant d’un plugin, il serait bien avisé de se prémunir d’éventuels conflits avec d’autres mappings existant.
Pour commencer, nous allons donc déclarer un mapping spécifique à notre
plugin en faisant usage du préfixe <Plug>
. On s’assurera par la même
occasion que la partie droite de notre mapping ne pourra faire l’objet d’un
mapping récursif en utilisant noremap
au lieu de map
.
xnoremap <Plug>DecodeQP :DecodeQP<CR>
nnoremap <Plug>DecodeQP :DecodeQP<CR>
Petite sécurité supplémentaire, définissons nos mappings uniquement si
<Plug>DecodeQP
n’a pas déjà été employé par ailleurs. Ainsi, si l’utilisateur
souhaite définir ses propres séquences, nous ne créerons pas de nouveaux
mappings inutilement.
if !hasmapto('<Plug>DecodeQP')
xmap gcp <Plug>DecodeQP
nmap gcp <Plug>DecodeQP
endif
Bonus à la ligne
Soyons zélés et poussons le vice un peu plus loin ! Je vous propose d’ajouter un
dernier mapping sur la séquence gcpp
pour appliquer notre commande sur la
ligne courante uniquement.
nnoremap <silent> <Plug>DecodeQPLine :call <SID>qp()<CR>
if !hasmapto('<Plug>DecodeQP')
nmap gcpp <Plug>DecodeQPLine
endif
Petite particularité ici, nous faisons fi de l’intervalle et appelons
directement la méthode qp()
à laquelle on substitue le préfixe <SID>
à s:
,
un identifiant de script unique qui évitera toute collision avec une fonction
homonyme qui pourrait être déclarée au sein d’un autre script.
Détail supplémentaire, l’usage du mot-clé <silent>
taira tout message inutile,
sans quoi :call <SNR>139_qp()
apparaitrait dans la ligne de statut en bas de
l’écran.
Quelques vérifications
Afin de s’assurer que le plugin ne sera pas chargé par une version trop ancienne de Vim, et pour s’éviter un double chargement inutile, ajoutons quelques lignes en tête de notre script :
if exists("g:loaded_decodeqp") || v:version < 700
finish
endif
let g:loaded_decodeqp = 1
Voyez comme il est aisé de s’assurer de la compatibilité de notre script à
l’aide de la variable prédéfinie par Vim v:version
; ici 700
signifie que
l’on accepte le chargement dudit plugin par Vim 7.0 et supérieurs. Si cette
condition n’est pas respectée, l’interprétation de notre script prendra fin
immédiatement grâce à l’instruction finish
.
De même, si la variable globale g:loaded_decodeqp
a déjà été déclarée, c’est
que notre script a déjà été interprété et qu’il n’est nul besoin de répéter
l’opération.
Quoi d’neuf Doc ?
Pour parfaire notre plugin, tâchons de rédiger un peu de documentation pour
informer nos utilisateurs des nouvelles fonctionnalités de leur éditeur de texte
préféré ! Pour cela, rien de plus simple : dans un dossier doc/
, nous allons
créer un bête fichier texte nommé gpg_qp.txt
.
Quelques règles typographiques sont à respecter pour tirer parti des possibilités offertes par la documentation intégrée de Vim, car oui, la documentation de notre plugin sera accessible depuis Vim via la commande suivante :
:help gpg
Il s’agit là d’une fraction du nom de notre fichier ne portant pas à confusion,
dès lors nul besoin de rechercher gpg_qp.txt
, Vim peut se contenter de gpg
pour satisfaire à votre demande.
Notre documentation se présente sous cette forme :
*gpg_qp.txt* Decode Quoted-Printable text
Author: François Vantomme <akarzim@pm.me>
License: Same terms as Vim itself (see |license|)
Decode Quoted-Printable text in ASC files. Relies on g:decodeqp_command which
can be customized, otherwise a Perl regexp will be used.
*gcp*
gcp Decode the whole file.
*v_gcp*
{Visual}gcp Decode the highlighted lines.
*:DecodeQP*
:[range]DecodeQP Decode [range] lines. Defaults to the whole file.
vim:tw=78:et:ft=help:norl:
Notez les mots-clés encadrés d’étoiles, les liens encadrés de barres verticales et la dernière ligne qui donne quelques informations à Vim quant à la largeur de page et au type de fichier notamment. Le rendu dans l’éditeur sera le suivant :
À l’installation du plugin, la documentation sera automatiquement rendue disponible par votre gestionnaire de plugins et des tags seront générés de manière à pouvoir chercher de l’aide sur les mots-clés. Pour les curieux, voici les tags générés :
:DecodeQP gpg_qp.txt /*:DecodeQP*
gcp gpg_qp.txt /*gcp*
gpg_qp.txt gpg_qp.txt /*gpg_qp.txt*
v_gcp gpg_qp.txt /*v_gcp*
Grâce à ces tags, il nous est alors possible de demander de l’aide sur gcp
et Vim nous dirigera directement sur la section de la documentation
correspondante.
Le mot de la fin
Au final, nous nous retrouvons avec un petit plugin d’une vingtaine de lignes qui nous met à disposition une nouvelle commande qui lance une belle regexp sur une sélection de texte ou l’ensemble de notre fichier, avec quelques raccourcis clavier en prime, et au travers duquel nous avons pu découvrir les rudiments du langage VimL.
Voici notre plugin dans son ensemble :
" gpg_qp.vim - Decode Quoted-Printable text
" Maintainer: François Vantomme <akarzim@pm.me>
" Version: 0.5
if exists("g:loaded_decodeqp") || v:version < 700
finish
endif
let g:loaded_decodeqp = 1
if !exists("g:decodeqp_command")
let g:decodeqp_command = 'perl -p -e ''s/=\n//m;s/=([\dA-F]{2})/pack H2,$1/gie'''
endif
function s:qp() range abort
execute a:firstline . "," . a:lastline . "!" . g:decodeqp_command
endfunction
command -range=% DecodeQP <line1>,<line2>call <SID>qp()
xnoremap <Plug>DecodeQP :DecodeQP<CR>
nnoremap <Plug>DecodeQP :DecodeQP<CR>
nnoremap <silent> <Plug>DecodeQPLine :call <SID>qp()<CR>
if !hasmapto('<Plug>DecodeQP')
xmap gcp <Plug>DecodeQP
nmap gcp <Plug>DecodeQP
nmap gcpp <Plug>DecodeQPLine
endif
Et si vous souhaitez tout simplement installer ce plugin, vous le retrouverez sur VimAwesome et sur GitHub.
J’espère que ce petit tour vous a plu et vous a donné envie de pousser plus loin l’exploration de Vim et de son langage de script !