3

I'm thinking about exchanging some keys while in non insert-mode so that the bindings that I use most are easier to reach (i.e. on the home row).

How can I swap two keys without breaking the behaviors of my plugins' bindings ?

For instance, Tim Pope's surround plugin uses the s key so that the following is a valid command:

cs"'

How can I exchange s with a so that:

  1. s in normal mode enters insert mode in the same manner that a did before the exchange, and
  2. ca"' performs the same action as cs"' did before the exchange?

I googled about map, noremap, after and autocmd VimEnter * but still don't understand which one is appropriate.

In other words, how can I have a second keyboard layout, valid in normal (visual) mode but not in insert mode ?

  • I do some swapping (everywhere, not just normal mode) and you can see it here. Not sure how it would interact with plugins, but you can try it out. I know it has some limitations, for example if I swap 1 and !, f1 doesn't do f! (and I couldn't figure out what to do about it). – Shahbaz Jan 3 '17 at 22:33
  • 1
    Why do you want s to append and a to substitute? It seems like more trouble than it's worth to me. – Antony Jan 4 '17 at 3:26
  • @Shahbaz, you can use :lmap 1 ! and :set iminsert=1 to be able to use the mapping with f as well. – Octaviour Jan 4 '17 at 11:16
  • @Octaviour, I had seen lmap before, but not :iminsert. However, the bigger problem it creates is that if you use this, then it breaks macros. For example, if I record a macro in which I press f and 1 (which translates to f!, when I repeat the macro, it tries to do f1. So unless you have a solution for that too, using :lmap is more troublesome. (Maybe I should ask a real question for this) – Shahbaz Jan 4 '17 at 15:07
  • vi.stackexchange.com/q/10849/850 – Shahbaz Jan 4 '17 at 15:25
4

I think your best bet would be to remap everything that uses s in all modes. You are already aware of the command for normal mode I think, nnoremap. This changes the normal mode mapping only. Once you press a key like c you normally enter operator pending mode. Since you are not in normal mode anymore, the nnoremap has no effect here. Instead you should use onoremap to define the behavior in this mode.

Tim Pope's surround uses a slightly different way of defining the surround command which can be found in surround.vim in the plugin folder of this plugin. The relevant code is:

if !exists("g:surround_no_mappings") || ! g:surround_no_mappings
  nmap ds  <Plug>Dsurround
  nmap cs  <Plug>Csurround
  nmap cS  <Plug>CSurround
  nmap ys  <Plug>Ysurround
  nmap yS  <Plug>YSurround
  nmap yss <Plug>Yssurround
  nmap ySs <Plug>YSsurround
  nmap ySS <Plug>YSsurround
  xmap S   <Plug>VSurround
  xmap gS  <Plug>VgSurround
  if !exists("g:surround_no_insert_mappings") || ! g:surround_no_insert_mappings
    if !hasmapto("<Plug>Isurround","i") && "" == mapcheck("<C-S>","i")
      imap    <C-S> <Plug>Isurround
    endif
    imap      <C-G>s <Plug>Isurround
    imap      <C-G>S <Plug>ISurround
  endif
endif

From this it is clear that this plugin uses ordinary nmaps to create the mappings. If you put the code below in your .vimrc you should be good to go. This first tells the plugin to not define any mappings and then goes on to define the custom mappings.

let g:surround_no_mappings=1
nmap da  <Plug>Dsurround
nmap ca  <Plug>Csurround
nmap cA  <Plug>CSurround
nmap ya  <Plug>Ysurround
nmap yA  <Plug>YSurround
nmap yaa <Plug>Yssurround
nmap yAa <Plug>YSsurround
nmap yAA <Plug>YSsurround
xmap A   <Plug>VSurround
xmap gA  <Plug>VgSurround
if !exists("g:surround_no_insert_mappings") || ! g:surround_no_insert_mappings
  if !hasmapto("<Plug>Isurround","i") && "" == mapcheck("<C-A>","i")
    imap    <C-A> <Plug>Isurround
  endif
  imap      <C-G>a <Plug>Isurround
  imap      <C-G>A <Plug>ISurround
endif
4

EDIT @Octaviour's solution is way smarter, you should use it :-) Nonetheless I'll leave this answer here because I worked on it and because it contains some interesting part about the usage of <SID> which can still be interesting to read


I believe that actually your problem is more complex than you could think:

What you want is not a second layout: if you want ca to behave like cs, it means that you want to remap ca to call the function that cs usually calls.

Here it is tricky because the cs mapping is done by the vim surround script and this plugin doesn't provide a mapping override mechanism. Thus you'll need to use several tricks.


First let's check surround.vim code, especially the part where the mapping is done:

nmap cs  <Plug>Csurround
nnoremap <silent> <Plug>Csurround  :<C-U>call <SID>changesurround()<CR>

The first line shows that cs is mapped to an expression which is local to the plugin: <Plug>Csurround.

The second line show what <Plug>Csurround (and thus cs) is mapped to :<C-U>call <SID>changesurround()<CR>. It means that it is mapped to call the plugin local function changesurround().

This means that if we want to map ca to changesurround() we will need to get the <SID> of the surround.vim script.

See :h <SID> for more details about the <SID> variables.


To get the <SID> of a script we can use the following function (largely inspired by :h scriptnames-dictionary):

function! GetScriptSID(scriptName)
    " Get the output of ":scriptnames" in the scriptnames_output variable.
    let scriptnames_output = ''
    redir => scriptnames_output
    silent scriptnames
    redir END

    " Split the output into lines and parse each line
    " Looking for vimsurround
    let scripts = {}
    for line in split(scriptnames_output, "\n")
        " Only do non-blank lines.
        if line =~ '\S'
            " Get the first number in the line.
            let nr = matchstr(line, '\d\+')
            " Get the file name
            let name = split(substitute(line, '.\+:\s*', '', ''), '\')[-1]

            if name == a:scriptName
                return nr
            endif
        endif
    endfor
    unlet scriptnames_output
endfunction

That we should call with call GetScritpSID('surround.vim'). On my system it returns 31 because this plugin was the 31st script to be sourced at vim startup.


Now you should be able to enter the following command in the command line:

nnoremap <silent> ca :<C-U>call <SNR>31_changesurround()<CR>

Replace 31 by the result of your call to GetScriptSID

And you'll have ca behaving like cs


That's nice but we are only half way through what we want to do. The next step is to put this mapping in your vimrc. We are have 2 problems:

You could put the command with the hard coded SID in your vimrc like this:

nnoremap <silent> ca :<C-U>call <SNR>31_changesurround()<CR>

But if you install/remove a script the SID might change and your mapping won't work anymore.

So you could use execute to get the SID dynamically: you need to add the GetScriptSID() function to your vimrc along with this line:

execute 'nnoremap <silent> ca :<C-U>call <SNR>' . GetScriptSID('surround.vim') . '_changesurround()<CR>'

But here when you'll use the mapping Vim will yell at you that 0_changesurround() doesn't exists.

This is because the vimrc is sourced before the other scripts so your execution of scriptname in your vimrc will return 0 scripts.


To solve this problem you'll have to do the following:

function! RemapVimSurround()
    let surroundSID = GetScriptSID('surround.vim')

    execute 'nnoremap <silent> ca :<C-U>call <SNR>' . surroundSID . '_changesurround()<CR>'
    unmap cs
endfunction

autocmd VimEnter * call RemapVimSurround()

The autocommand will execute the function RemapVimSurround() once all the scripts are sourced. This way the scriptname execution in the function will return the correct SID and your mapping will be valid. Also we unmap cs which was mapped by the plugin.


Finally you also wanted to have s behave like a in normal mode so you should be able to use:

nnoremap s a

To sum up you should add the following to your vimrc:

function! GetScriptSID(scriptName)
    " Get the output of ":scriptnames" in the scriptnames_output variable.
    let scriptnames_output = ''
    redir => scriptnames_output
    silent scriptnames
    redir END

    " Split the output into lines and parse each line
    " Looking for vimsurround
    let scripts = {}
    for line in split(scriptnames_output, "\n")
        " Only do non-blank lines.
        if line =~ '\S'
            " Get the first number in the line.
            let nr = matchstr(line, '\d\+')
            " Get the file name
            let name = split(substitute(line, '.\+:\s*', '', ''), '\')[-1]

            if name == a:scriptName
                return nr
            endif
        endif
    endfor
    unlet scriptnames_output
endfunction


function! RemapVimSurround()
    let surroundSID = GetScriptSID('surround.vim')

    execute 'nnoremap <silent> ca :<C-U>call <SNR>' . surroundSID . '_changesurround()<CR>'
    unmap cs
    nnoremap s a
endfunction

autocmd VimEnter * call RemapVimSurround()
  • 1
    Complex but still very insightful (+1) – Julien__ Jan 4 '17 at 11:57
  • Yes this is clearly overcomplicated but I think that can be a good way to learn more about variables and functions scopes in vimscript. – statox Jan 4 '17 at 12:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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