0

I'm trying to make a small extension/bug fix to a plugin I rely on (referenced as Plugin in the code snips), therefore I'm constrained by its existing API.

I've got the functionality I want somewhat working, however due to inconsistent typing to what the Plugin's API is expecting, it is not quite working fully.

 

Code

So here's my somewhat functional code addition to Plugin; with the context explained beneath:

function! s:conditional_func(mode, ActionFnRef, key)
    if a:mode.Enabled
        call a:ActionFnRef(a:key)
    else
        call Plugin#Pass#Action(a:key)
    endif
endfunction


function! s:nop_exception_register(mode, key)
    let existing_keymap = Plugin#Get#Keymap()
    let ActionFnRef     = existing_keymap[a:mode][a:key]

    call Plugin#Keymap#Register(function('s:conditional_func',
                                       \ [a:mode, ActionFnRef]),
                              \ "Nop", a:key)
endfunction


" s:nop_exception_register is later called via another function

In s:conditional_func:

  • a:mode.Enabled is a boolean for the status of a:mode

  • a:ActionFnRef will be some existing Plugin function, which I only want called (with its own "key-pressed" argument a:key) when a:mode.Enabled == 1

  • Plugin#Pass#Action is a Plugin function that effectively performs no operation when passed a:key (in practice, it provides the user with a feedback message, but that's unimportant here)

In s:nop_exception_register:

  • the Plugin#Get#Keymap() returns a dict of all existing keymaps, grouped by their modes, then by the actual keys within each mode

  • thereby the actual function defining the action to be performed when a key is pressed in its mode is captured by the assignment to ActionFnRef

  • Plugin#Keymap#Register is the built-in API call for registering actions and their keys in the correct mode

 

Problem Description

The function signature for Plugin#Keymap#Register is actually (v:t_string, v:t_string, v:t_string), and NOT (v:t_func, v:t_string, v:t_string).

Normally, the name of the action function is passed as a string to Plugin#Keymap#Register, which generates the Funcref itself by calling function("actionFuncName").

I'm able to have the action working as desired (i.e. conditionally depending on the value of s:mode.Enabled) because I guess calling function with a Funcref like I did just returns the same Funcref.

However Plugin#Keymap#Register also generates a dict map of help descriptions, which uses the passed string names of the action functions as its keys. Therefore passing it a Funcref like I did eventually causes Plugin code executed downstream of that to error. The help map can still be successfully initialized, which I've confirmed by inspecting it. It will contain items like:

  • 'A': 'Plugin#DoActionW'
  • 'B': 'Plugin#DoActionX'
  • 'C': function('Plugin#DoActionY')
  • 'D': function('Plugin#DoActionZ')

Where items keyed by 'A' & 'B' will work correctly, but 'C' & 'D' (added by my hack) not quite.

 

Attempted Workarounds

  1. Create a named Partial inside s:nop_exception_register, and trying to pass its name as a string to Plugin#Keymap#Register
function! s:nop_exception_register(mode, key)
    let existing_keymap = Plugin#Get#Keymap()
    let ActionFnRef     = existing_keymap[a:mode][a:key]

    let CondPartial = function('s:conditional_func', [a:mode, ActionFnRef])
    call Plugin#Keymap#Register("CondPartial", "Nop", a:key)
endfunction

This causes a Vim error before it's even fully loaded, with E700: Unknown function: CondPartial displayed in the terminal.

  1. Place the logic of 's:conditional_func' inside of s:nop_exception_register as a closure and pass that to Plugin#Keymap#Register
function! s:nop_exception_register(mode, key)
    let existing_keymap = Plugin#Get#Keymap()
    let ActionFnRef     = existing_keymap[a:mode][a:key]

    func! s:_cond_call_action(key) closure
        if a:mode.Enabled
            call ActionFnRef(a:key)
        else
            call Plugin#Pass#Action(a:key)            
        endif
    endfunc

    call Plugin#Keymap#Register("s:_cond_call_action", "Nop", a:key)
endfunction

Same error: E700: Unknown function: s:_cond_call_action

  1. Also tried to use eval(), something like string(eval("function('s:cond_return_func', [a:mode, ActionFnRef])")), where s:cond_return_func basically returns a Partial like CondPartial from workaround 1 above. This also errors, not on Vim startup, but at runtime when the relevant part of the Plugin code is called: E117: Unknown function: <evaluated representation of s:cond_return_func>

  2. I've also considered "dereferencing" a Funcref to its string name if somehow possible, like how in Python one can do <funcname>.__name__ to get the string identifier of the function. Though intuitively this seems unlikely to be possible in Vimscript, as I don't believe functions/funcrefs in Vimscript are like objects in Python. But ofc, please correct me if I'm simply being ignorant here.

 

So basically, to me it looks like my problem is not fully understanding how to resolve variable/function scopes in Vimscript.

Also I know the two functions from the first code snippet I showed can almost certainly be refactored into one, which I plan on doing, once this more glaring issue is resolved.

I apologize in advance for this long and verbose post, but I just wanted to make sure the problem is fully described. I'm pretty much at my wit's end, please help.

3
  • I think you’re intended to pass the name of a function, and then call function() or funcref() in the later handler to get a real function.
    – D. Ben Knoble
    Nov 15, 2019 at 16:07
  • Yes that is how the API in question is suppose to work. However, for my specific purpose, I need the function passed to the API only callable when Plugin is a certain state (when a:mode.Enabled == 1 to be precise), which this is not always the case, therefore I needed the function to be modified to take into account of this state information. Which is why I ended up using a Partial, and considered using a closure as well.
    – Jethro Cao
    Nov 15, 2019 at 18:42
  • Couldnt the function be always callable, but be unspecified if the mode is not checked prior to calling? Iow, pass the function either way, and clients are responsible for validating the state first?
    – D. Ben Knoble
    Nov 15, 2019 at 18:48

1 Answer 1

3

From what I can tell without seeing its implementation, Plugin#Keymap#Register expects a string referring to a globally accessible function, which limits your options. A closure with a single name won't quite work because its contents would keep changing.

One option is to create a dynamically-named function for each value of outer key.

function! s:nop_exception_register(mode, key)
    let existing_keymap = Plugin#Get#Keymap()
    let ActionFnRef     = existing_keymap[a:mode][a:key]

    func! Cond_call_action_{a:key}(key_) closure
        if a:mode.Enabled
            call ActionFnRef(a:key_)
        else
            call Plugin#Pass#Action(a:key_)            
        endif
    endfunc

    call Plugin#Keymap#Register('Cond_call_action_' + a:key, 'Nop', a:key)
endfunction

This makes each instance an actual global function, i.e., you could :call Cond_call_action_whatever(..) from the cmdline if you wanted.

It is technically possible, though difficult, to use a s: function here.

1
  • 1
    Thanks for your try and general advice. You're right in that using a global scoped function to generate the partial is better, which I did. But I ended up using get(<funcref>, 'name') to get the name of the Partial funcref as a string, and use that to key the help dict, this way I could just add one entry to this other help-description map instead of multiple, especially since I don't necessarily know before hand how many of these excepted actions might be needed eventually.
    – Jethro Cao
    Nov 15, 2019 at 14:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.