Est-il possible d'utiliser un délégué ou de passer une fonction comme argument dans Vimscript?

11

J'essaie de créer un petit plugin pour apprendre vimscript, mon objectif est de créer des fonctions traitant un texte sélectionné et le remplaçant par le résultat. Le script contient les éléments suivants:

  • Deux fonctions de traitement de texte: elles prennent une chaîne en paramètre et retournent la chaîne qui doit être utilisée pour remplacer le texte d'origine. Pour l'instant, je n'en ai que deux, mais il pourrait y en avoir beaucoup plus dans quelques temps.

  • Une fonction récupérant le texte sélectionné: qui tire simplement la dernière sélection et la renvoie.

  • Une fonction wrapper: qui appelle une fonction de traitement, obtient son résultat et remplace l'ancienne sélection par ce résultat.

Pour l'instant, ma fonction wrapper ressemble à ceci:

function! Wrapper()
    " Get the string to insert
    let @x = Type1ProcessString(GetSelectedText())

    " remove the old selection
    normal gvd

    " insert the new string
    normal "xp
endfunction

Et je dois créer un deuxième wrapper remplaçant la ligne 3 par

let @x = Type2ProcessString(GetSelectedText())

Je voudrais donner à ma fonction wrapper un paramètre contenant la fonction Process pour exécuter et utiliser un appel générique à la ligne 3. Pour l'instant, j'ai essayé d'utiliser calldifférentes manières comme, par exemple, ceci:

let @x = call('a:functionToExecute', GetSelectedText()) 

mais je n'ai pas vraiment réussi et je n'ai pas été :h callvraiment utile sur le sujet des délégués.

Pour résumer, voici mes questions:

  • Comment puis-je créer une seule fonction wrapper pour toutes les fonctions de traitement?
  • Y a-t-il quelque chose qui fonctionne en tant que délégué dans vimscript?
  • Si les délégués n'existent pas, quelle serait une "bonne" façon de faire ce que je veux?
statox
la source

Réponses:

16

Pour répondre à votre question: le prototype de call()dans le manuel est call({func}, {arglist} [, {dict}]); l' {arglist}argument doit être littéralement un objet List, pas une liste d'arguments. Autrement dit, vous devez l'écrire comme ceci:

let @x = call(a:functionToExecute, [GetSelectedText()])

Cela suppose a:functionToExecutesoit un Funcref (voir :help Funcref), soit le nom d'une fonction (c'est-à-dire une chaîne, par exemple 'Type1ProcessString').

Maintenant, c'est une fonctionnalité puissante qui donne à Vim une sorte de qualité semblable à LISP, mais vous l'utiliseriez probablement rarement comme ci-dessus. Si a:functionToExecuteest une chaîne, le nom d'une fonction, vous pouvez le faire:

function! Wrapper(functionToExecute)
    " ...
    let s:processing = function(a:functionToExecute)
    let @x = s:processing(GetSelectedText())
    " ...
endfunction

et vous appelleriez le wrapper avec le nom de la fonction:

call Wrapper('Type1ProcessString')

Si d'autre part a:functionToExecuteest un Funcref, vous pouvez l'appeler directement:

function! Wrapper(functionToExecute)
    " ...
    let @x = a:functionToExecute(GetSelectedText())
    " ...
endfunction

mais vous devez appeler le wrapper comme ceci:

call Wrapper(function('Type1ProcessString'))

Vous pouvez vérifier l'existence de fonctions avec exists('*name'). Cela rend possible la petite astuce suivante:

let s:width = function(exists('*strwidth') ? 'strwidth' : 'strlen')

c'est-à-dire une fonction qui utilise la fonction intégrée strwidth()si Vim est suffisamment nouvelle pour l'avoir, et revient à strlen()autre chose (je ne dis pas qu'une telle solution de repli est logique; je dis simplement que cela peut être fait). :)

Avec les fonctions de dictionnaire (voir :help Dictionary-function), vous pouvez définir quelque chose qui ressemble à des classes:

let g:MyClass = {}

function! g:MyClass.New(...)
    let newObj = copy(self)

    if a:0 && type(a:1) == type({})
        let newObj._attributes = deepcopy(a:1)
    endif
    if exists('*MyClassProcess')
        let newObj._process = function('MyClassProcess')
    else
        let newObj._process = function('s:_process_default')
    endif

    return newObj
endfunction

function! g:MyClass.getFoo() dict
    return get(get(self, '_attributes', {}), 'foo')
endfunction

function! g:MyClass.setFoo(val) dict
    if !has_key(self, '_attributes')
        let self._attributes = {}
    endif
    let self._attributes['foo'] = a:val
endfunction

function! g:MyClass.process() dict
    call self._process()
endfunction

function! s:_process_default()
    echomsg 'nothing to see here, define MyClassProcess() to make me interesting'
endfunction

Ensuite, vous instanciez des objets comme celui-ci:

let little_object = g:MyClass.New({'foo': 'bar'})

Et appelez ses méthodes:

call little_object.setFoo('baz')
echomsg little_object.getFoo()
call little_object.process()

Vous pouvez également avoir des attributs et des méthodes de classe:

let g:MyClass.__meaning_of_life = 42

function g:MyClass.GetMeaningOfLife()
    return get(g:MyClass, '__meaning_of_life')
endfunction

(remarquez pas besoin dictici).

Edit: Le sous-classement est quelque chose comme ceci:

let g:MySubclass = copy(g:MyClass)
call extend(g:MySubclass, subclass_attributes)

Le point subtil ici est l'utilisation de copy()au lieu de deepcopy(). La raison en est de pouvoir accéder aux attributs de la classe parente par référence. Cela peut être réalisé, mais c'est très fragile et le faire est loin d'être trivial. Un autre problème potentiel est que ce type de sous-classe se confond is-aavec has-a. Pour cette raison, les attributs de classe ne valent généralement pas vraiment la peine.

D'accord, cela devrait suffire à vous donner matière à réflexion.

Retour à votre extrait de code initial, il y a deux détails qui pourraient être améliorés:

  • vous n'avez pas besoin normal gvdde supprimer l'ancienne sélection, la normal "xpremplacera même si vous ne la tuez pas en premier
  • utiliser call setreg('x', [lines], type)au lieu de let @x = [lines]. Cela définit explicitement le type du registre x. Sinon, vous comptez xdéjà sur le bon type (c.-à-d. Par caractère, par ligne ou par bloc).
lcd047
la source
Lorsque vous créez directement des fonctions dans un dictionnaire (c'est-à-dire une "fonction numérotée"), vous n'avez pas besoin du dictmot - clé. Cela s'applique à vos "méthodes de classe". Tu vois :h numbered-function.
Karl Yngve Lervåg
@ KarlYngveLervåg Techniquement , il applique aux deux méthodes de classe et l' objet (il n'y a pas besoin de dictpour l' une des MyClassfonctions). Mais je trouve cela déroutant, j'ai donc tendance à ajouter dictexplicitement.
lcd047
Je vois. Vous ajoutez donc dictdes méthodes d'objet, mais pas des méthodes de classe, afin de clarifier votre intention?
Karl Yngve Lervåg
@ lcd047 Merci beaucoup pour cette réponse incroyable! Je vais devoir y travailler mais c'est exactement ce que je cherchais!
statox
1
@ KarlYngveLervåg Il y a une subtilité ici, la signification de selfest différente pour les méthodes de classe et pour les méthodes d'objet - c'est la classe elle-même dans le premier cas, et l'instance de l'objet actuel dans le second. Pour cette raison, je me réfère toujours à la classe elle-même comme g:MyClass, ne l'utilisant jamais self, et je vois principalement le dictcomme un rappel qu'il est correct d'utiliser self(c'est-à-dire une fonction qui a dicttoujours agi sur une instance d'objet). Encore une fois, je n'utilise pas beaucoup les méthodes de classe, et quand je le fais, j'ai également tendance à omettre dictpartout. Oui, l'auto-cohérence est mon deuxième prénom. ;)
lcd047
1

Générez la commande dans une chaîne et utilisez-la :exepour l'exécuter. Voir :help executepour plus de détails.

Dans ce cas, executepermet d'appeler la fonction et de mettre le résultat dans le registre, les différents éléments de la commande doivent être concaténés avec l' .opérateur sous forme de chaîne régulière. La ligne 3 devrait alors devenir:

execute "let @x = " . a:functionToExecute . "(GetSelectedText())"
cxw
la source