18

I am developing a plugin for Vim and I would like to define a mapping which would be available only while the "execution of the plugin".

So far the (simplified) workflow of the plugin is the following:

  1. The user call a command of the plugin
  2. The command calls the pre-treatment function:

    function! s:PreTreatmentFunction(function, ...)
        " Do some pretreatment stuff
    
        " Create a mapping to call the TearDown
        nnoremap <C-c> :call TeardDown()<CR>
    
        " Call a function depending on the parameter passed to this one
        if function == "foo"
            call Foo()
        else
            call Bar()
        endif
    endfunction
    
  3. Another function is called which change the state of the buffer (Foo() or Bar() in the last lines of the previous function)

  4. The user use the mapping to call the teardown function
  5. The tear down function remove the created mapping:

    function! s:TearDown()
        " Do some tear down stuff
    
        " Remove the mapping
        unmap <C-c>
    endfunction
    

I am not satisfied with the way I handle my mapping: if the user already mapped it to something else he will loose his original mapping.

So my question is: How can I save what <C-c> is mapped to (if it is mapped) and restore it in my tear down function? Is there a built-in feature to do so? I though about greping the result of :nmap <C-c> but that doesn't feel really "clean".

A few side notes:

  • I know that LearnVimScriptTheHardWay has a section about that, but they say to use a ftplugin which is not possible here: the plugin is not dependent on a file type
  • I could create a variable to let the user choose what keys to use: It is probably what I will do but I'm mainly interested in how to do the save and restore.
  • I could use a local leader but I think it's a little bit overkill and I'm still mainly curious about the save and restore thing.

2 Answers 2

34

You could use the maparg() function.

To test if the user mapped something to <C-c> in normal mode, you would write:

if !empty(maparg('<C-c>', 'n'))

If the user mapped something, to store the {rhs} in a variable, you would write:

let rhs_save = maparg('<C-c>', 'n')

If you want more information about the mapping, like:

  • is it silent (<silent> argument)?
  • is it local to the current buffer (<buffer> argument)?
  • is the {rhs} the evaluation of an expression (<expr> argument)?
  • does it remap the {rhs} (nnoremap vs nmap)?
  • if the user has another mapping which begins with <C-c>, does Vim wait for more characters to be typed (<nowait> argument)?
  • ...

Then, you could give a third and a fourth argument: 0 and 1.
0 because you're looking for a mapping and not an abbreviation, and 1 because you want a dictionary with a maximum of information and not just the {rhs} value:

let map_save = maparg('<C-c>', 'n', 0, 1)

Assuming the user didn't use any special argument in his mapping, and that it doesn't remap the {rhs}, to restore it, you could simply write:

let rhs_save = maparg('<C-c>', 'n')

" do some stuff which changes the mapping

exe 'nnoremap <C-c> ' . rhs_save

Or to be sure and restore all the possible arguments:

let map_save = maparg('<C-c>', 'n', 0, 1)

" do some stuff which changes the mapping

exe (map_save.noremap ? 'nnoremap' : 'nmap') .
     \ (map_save.buffer ? ' <buffer> ' : '') .
     \ (map_save.expr ? ' <expr> ' : '') .
     \ (map_save.nowait ? ' <nowait> ' : '') .
     \ (map_save.silent ? ' <silent> ' : '') .
     \ ' <C-c> ' .
     \ map_save.rhs

Edit: Sorry, I've just realised it wouldn't work as expected if the user calls a script-local function in the {rhs} of the mapping.

Suppose that the user has the following mapping inside his vimrc:

nnoremap <C-c> :<C-U>call <SID>FuncA()<CR>

function! s:FuncA()
    echo 'hello world!'
endfunction

When he hits <C-c>, it displays the message hello world!.

And in your plugin, you save a dictionary with all the information, then temporarily change his mapping like this:

let map_save = maparg('<C-c>', 'n', 0, 1)
nnoremap <C-c> :<C-U>call <SID>FuncB()<CR>

function! s:FuncB()
    echo 'bye all!'
endfunction

Now, it will display bye all!. Your plugin does some work, and when it's over, it tries to restore the mapping with the previous command.

It will probably fail with a message looking like this:

E117: Unknown function: <SNR>61_FuncA

61 is just the identifier of the script in which your mapping command would be executed. It could be any other number. If your plugin is the 42th file sourced on the user's system, it will be 42.

Inside a script, when a mapping command is executed, Vim automatically translates the notation <SID> into the special key code <SNR>, followed by a number which is unique for the script, and an underscore. It has to do this, because when the user will hit <C-c>, the mapping will be executed outside of the script, and thus it won't know in which script FuncA() is defined.

The problem is that the original mapping was sourced in a different script than your plugin, so here the automatic translation is wrong. It uses the identifier of your script, while it should use the identifier of the user's vimrc.

But you could do the translation manually. The dictionary map_save contains a key called 'sid' whose value is the correct identifier.
So, to make the previous restoration command more robust, you could replace map_save.rhs with:

substitute(map_save.rhs, '<SID>', '<SNR>' . map_save.sid . '_', 'g')

If the {rhs} of the original mapping contained <SID>, it should be properly translated. Otherwise, nothing should be changed.

And if you want to shorten the code a little, you could replace the 4 lines which take care of the special arguments with:

join(map(['buffer', 'expr', 'nowait', 'silent'], 'map_save[v:val] ? "<" . v:val . ">": ""'))

The map() function should convert each item from the list ['buffer', 'expr', 'nowait', 'silent'] into the corresponding mapping argument but only if its key inside map_save is non-zero. Andjoin() should join all the items into a string.

So, a more robust way of saving and restoring the mapping could be:

let map_save = maparg('<C-c>', 'n', 0, 1)

" do some stuff which changes the mapping

exe (map_save.noremap ? 'nnoremap' : 'nmap') .
    \ join(map(['buffer', 'expr', 'nowait', 'silent'], 'map_save[v:val] ? "<" . v:val . ">": ""')) .
    \ map_save.lhs . ' ' .
    \ substitute(map_save.rhs, '<SID>', '<SNR>' . map_save.sid . '_', 'g')

Edit2:

I'm facing the same issue as you, how to save and restore a mapping in a drawing plugin. And I think I found 2 issues that the initial answer didn't see at the time I wrote it, sorry about that.

First issue, suppose that the user uses <C-c> in a global mapping but also in a buffer-local mapping. Example:

nnoremap          <C-c>    :echo 'global mapping'<CR>
nnoremap <buffer> <C-c>    :echo 'local  mapping'<CR>

In this case, maparg() will give priority to the local mapping:

:echo maparg('<C-c>', 'n', 0, 1)

---> {'silent': 0, 'noremap': 1, 'lhs': '<C-C>', 'mode': 'n', 'nowait': 0, 'expr': 0, 'sid': 7, 'rhs': ':echo ''local  mapping''<CR>', 'buffer': 1}

Which is confirmed in :h maparg():

    The mappings local to the current buffer are checked first,
    then the global mappings.

But maybe you aren't interested in the buffer-local mapping, maybe you want the global one.
The only way I found to, reliably, get the information about the global mapping, is to try to temporarily unmap a potential, shadowing, buffer-local mapping using the same key.

It could be done in 4 steps:

  1. save a (potential) buffer-local mapping using the key <C-c>
  2. execute :silent! nunmap <buffer> <C-c> to delete a (potential) buffer-local mapping
  3. save the global mapping (maparg('<C-c>', 'n', 0, 1))
  4. restore the buffer-local mapping

The second issue is the following. Suppose that the user didn't map anything to <C-c>, then the output of maparg() will be an empty dictionary. And in this case, the restoration process doesn't consist in the installation of a mapping (:nnoremap), but in the destruction of a mapping (:nunmap).

To try to solve these 2 new issues, you could try this function to save mappings:

fu! Save_mappings(keys, mode, global) abort
    let mappings = {}

    if a:global
        for l:key in a:keys
            let buf_local_map = maparg(l:key, a:mode, 0, 1)

            sil! exe a:mode.'unmap <buffer> '.l:key

            let map_info        = maparg(l:key, a:mode, 0, 1)
            let mappings[l:key] = !empty(map_info)
                                \     ? map_info
                                \     : {
                                        \ 'unmapped' : 1,
                                        \ 'buffer'   : 0,
                                        \ 'lhs'      : l:key,
                                        \ 'mode'     : a:mode,
                                        \ }

            call Restore_mappings({l:key : buf_local_map})
        endfor

    else
        for l:key in a:keys
            let map_info        = maparg(l:key, a:mode, 0, 1)
            let mappings[l:key] = !empty(map_info)
                                \     ? map_info
                                \     : {
                                        \ 'unmapped' : 1,
                                        \ 'buffer'   : 1,
                                        \ 'lhs'      : l:key,
                                        \ 'mode'     : a:mode,
                                        \ }
        endfor
    endif

    return mappings
endfu

... and this one to restore them:

fu! Restore_mappings(mappings) abort

    for mapping in values(a:mappings)
        if !has_key(mapping, 'unmapped') && !empty(mapping)
            exe     mapping.mode
               \ . (mapping.noremap ? 'noremap   ' : 'map ')
               \ . (mapping.buffer  ? ' <buffer> ' : '')
               \ . (mapping.expr    ? ' <expr>   ' : '')
               \ . (mapping.nowait  ? ' <nowait> ' : '')
               \ . (mapping.silent  ? ' <silent> ' : '')
               \ .  mapping.lhs
               \ . ' '
               \ . substitute(mapping.rhs, '<SID>', '<SNR>'.mapping.sid.'_', 'g')

        elseif has_key(mapping, 'unmapped')
            sil! exe mapping.mode.'unmap '
                                \ .(mapping.buffer ? ' <buffer> ' : '')
                                \ . mapping.lhs
        endif
    endfor

endfu

The Save_mappings() function could be used to save mappings.
It expects 3 arguments:

  1. a list of keys; example: ['<C-a>', '<C-b>', '<C-c>']
  2. a mode; example: n for normal mode or x for visual mode
  3. a boolean flag; if it's 1, it means you're interested in global mappings, and if it's 0, in local ones

With it, you could save the global mappings using the keys C-a, C-b and C-c, in normal mode, inside a dictionary:

let your_saved_mappings = Save_mappings(['<C-a>', '<C-b>', '<C-c>'], 'n', 1)

Then, later, when you'll want to restore the mappings, you could call Restore_mappings(), passing the dictionary containing all the info as an argument:

call Restore_mappings(your_saved_mappings)

There could be a 3rd problem, when saving/restoring buffer-local mappings. Because, between the moment when we saved the mappings, and the moment when we try to restore them, the current buffer may have changed.

In this case, maybe the Save_mappings() function could be improved by saving the number of the current buffer (bufnr('%')).

And then, Restore_mappings() would use this info to restore the buffer-local mappings in the right buffer. We could probably use the :bufdo command, prefix the latter with a count (matching the buffer number previously saved), and suffix it with the mapping command.

Maybe something like:

:{original buffer number}bufdo {mapping command}

We would have to check first if the buffer still exists, using the bufexists() function, because it could have been deleted in the meantime.

1
  • 1
    Amazing that's exactly what I needed. Thanks!
    – statox
    Commented May 11, 2016 at 11:51
2

In my plugins, when I have temporary mappings, they are always buffer local -- I really don't care about saving global mappings nor about anything complex that involve them. Hence my lh#on#exit().restore_buffer_mapping() helper function -- from lh-vim-lib.

In the end, what happens is the following:

" excerpt from autoload/lh/on.vim
function! s:restore_buffer_mapping(key, mode) dict abort " {{{4
  let keybinding = maparg(a:key, a:mode, 0, 1)
  if get(keybinding, 'buffer', 0)
    let self.actions += [ 'silent! call lh#mapping#define('.string(keybinding).')']
  else
    let self.actions += [ 'silent! '.a:mode.'unmap <buffer> '.a:key ]
  endif
  return self
endfunction

" The action will be executed later on with:
" # finalizer methods {{{2
function! s:finalize() dict " {{{4
  " This function shall not fail!
  for l:Action in self.actions
    try
      if type(l:Action) == type(function('has'))
        call l:Action()
      elseif !empty(l:Action)
        exe l:Action
      endif
    catch /.*/
      call lh#log#this('Error occured when running action (%1)', l:Action)
      call lh#log#exception()
    finally
      unlet l:Action
    endtry
  endfor
endfunction


" excerpt from autoload/lh/mapping.vim
" Function: lh#mapping#_build_command(mapping_definition) {{{2
" @param mapping_definition is a dictionary witch the same keys than the ones
" filled by maparg()
function! lh#mapping#_build_command(mapping_definition)
  let cmd = a:mapping_definition.mode
  if has_key(a:mapping_definition, 'noremap') && a:mapping_definition.noremap
    let cmd .= 'nore'
  endif
  let cmd .= 'map'
  let specifiers = ['silent', 'expr', 'buffer']
  for specifier in specifiers
    if has_key(a:mapping_definition, specifier) && a:mapping_definition[specifier]
      let cmd .= ' <'.specifier.'>'
    endif
  endfor
  let cmd .= ' '.(a:mapping_definition.lhs)
  let rhs = substitute(a:mapping_definition.rhs, '<SID>', "\<SNR>".(a:mapping_definition.sid).'_', 'g')
  let cmd .= ' '.rhs
  return cmd
endfunction

Your Answer

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

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