2

Context: I learned about the :[v]split command long before I learned the shortcuts Ctrl-w+[s|v] and as a result I find myself typing these commands often. Is there a way to break this habit ? (as a similar example, when I was first learning vim I mapped the arrow keys to nop to force me to use hjkl.

The catch is I'd like :[v]split <...> to still work.

Generically, do you have any recommendations to force the use of keybindings rather than the equivalent command mode commands ?

2 Answers 2

8

You could try to add this code in your vimrc:

let g:my_overlooked_commands = [
                            \   { 'old': 'vsplit', 'new': 'C-w v' },
                            \   { 'old': 'split',  'new': 'C-w s' },
                            \   { 'old': 'q!',     'new': 'ZQ' },
                            \   { 'old': 'x',      'new': 'ZZ' },
                            \]

fu! s:alternative_reminder(cmd) abort
    call timer_start(0, {->
    \ execute('echohl WarningMsg | echo "['.a:cmd.'] was equivalent" | echohl NONE', '') })
    return ''
endfu

fu! s:remember(list) abort
    for cmd in a:list
        let old = cmd.old
        let sold = string(old)
        exe printf('
        \            cnorea <expr> %s getcmdtype() ==# ":" && getcmdline() ==# %s
        \?                   %s.<sid>alternative_reminder(%s)
        \:                   %s
        \',          old, sold, sold, string(cmd.new), sold
        \         )
    endfor
endfu

call s:remember(g:my_overlooked_commands)

To use it, inside the list g:my_overlooked_commands, you would need to add the old commands you're currently using, and their new counterparts you would like to use, but frequently forget to.

After executing an old command, a message should be displayed in the command-line to remind you that there's an alternative.


Edit: Concerning these 2 questions:

  1. How does the %s.alternative_reminder(%s) part work?

printf() expects a string as its first argument (called {fmt} by the help, probably as an abbreviation for format), and one or more expressions. The format (first argument) can contain one or several % items. There are several types of items, %s is one of them. The s stands for string. It means that you want printf() to replace it with the value of an expression whose type is a string. Here's an example:

:let var = 'string' | echo printf('here is a %s', var)

Going back to the code, the contents of the format is:

cnorea <expr> %s getcmdtype() ==# ":" && getcmdline() ==# %s
\?                   %s.<sid>alternative_reminder(%s)
\:                   %s

It contains 5 %s items. So, printf() expects 5 more arguments after the format. Here they are given on the next line:

old, sold, sold, string(cmd.new), sold

The values of these expressions will replace the items in the format, respecting the order in which they are passed to printf().

So, this command:

exe printf('
\            cnorea <expr> %s getcmdtype() ==# ":" && getcmdline() ==# %s
\?                   %s.<sid>alternative_reminder(%s)
\:                   %s
\',          old, sold, sold, string(cmd.new), sold
\         )

Should be (untested) equivalent to something like this:

exe 'cnorea <expr> '.old.' getcmdtype() ==# ":" && getcmdline() ==# '.sold
\.              '?     '.sold.'.<sid>alternative_reminder('.string(cmd.new).')'
\.              ':     '.sold

You don't need to invoke the printf() function. But as soon as I get a concatenation of strings and other expressions, I find that using printf() makes the code a little more readable.

Once printf() has done its job, :execute should receive a string looking like this:

    cnorea <expr> vsplit getcmdtype() ==# ":" && getcmdline() ==# 'vsplit'
                  \?     'vsplit'.<sid>alternative_reminder('vsplit')
                  \:     'vsplit'

This is an abbreviation which expands the word vsplit into the value of an expression, which uses the ternary operator ?:. If the test:

getcmdtype() ==# ":" && getcmdline() ==# 'vsplit'

… is true, then, the value of the expression is:

'vsplit'.<sid>alternative_reminder('vsplit')

… otherwise:

'vsplit'

Note that <sid>alternative_reminder('vsplit') is an expression whose value is the one returned at the end of the function. By default, a function returns the number 0. But here the function explicitly returns an empty string. So, the function doesn't add anything to the expansion of vsplit, because it's replaced with an empty string. However, to get to the returned value, Vim must process the code of the function. The latter contains some logic to trigger a warning message when necessary.

The goal of the test is to prevent the reminder to be triggered if you're not on a regular Ex command-line, or if you're not at the beginning of the command-line.

Basically, vsplit should always be expanded into vsplit (unchanged), but the reminder should be triggered when you execute the command at the beginning of a regular Ex command-line


  1. Is it possible to adapt this to actually prevent the split from occurring? Removing the %s. from the quoted line above almost achieves this, but it breaks the :vsplit case.

I haven't tested it very long and I don't know how reliable it is, but you could try this code:

let g:my_overlooked_commands = [
                            \   { 'old': 'vsplit', 'new': 'C-w v' },
                            \   { 'old': 'split',  'new': 'C-w s' },
                            \   { 'old': 'q!',     'new': 'ZQ' },
                            \   { 'old': 'x',      'new': 'ZZ' },
                            \]

fu! s:alternative_reminder(old, new) abort
    if getcmdline()[-1:-1] ==# ' '
        call feedkeys("\<bs>".a:old.' ', 'int')
    else
        call timer_start(0, {->
        \ execute('echohl WarningMsg | echo "['.a:new.'] was equivalent" | echohl NONE', '') })
    endif
    return ''
endfu

fu! s:remember(list) abort
    for cmd in a:list
        let old = cmd.old
        let sold = string(old)
        exe printf('
        \            cnorea <expr> %s getcmdtype() ==# ":" && getcmdline() ==# %s
        \?                   timer_start(0, {-> execute("call <sid>alternative_reminder(%s, %s)") }) ? "" : ""
        \:                   %s
        \',          old, sold, sold, string(cmd.new), sold
        \         )
    endfor
endfu

call s:remember(g:my_overlooked_commands)
5
  • Nice! I have two questions about this: 1. How does the %s.<sid>alternative_reminder(%s) part work? 2. Is it possible to adapt this to actually prevent the split from occurring? Removing the %s. from the quoted line above almost achieves this, but it breaks the :vsplit <file> case.
    – Rich
    Oct 12, 2017 at 9:07
  • Thanks for the long explanation. Unfortunately, I wasn't clear enough in how I asked my question. What I don't understand is how the expanded 'vsplit'.<sid>alternative_reminder('vsplit') results in the desired behaviour. Typing :vsplitfunction() into the command-line doesn't work, so how does returning this as the result of an <expr> abbrev result in the desired functionality?
    – Rich
    Oct 12, 2017 at 10:28
  • @Rich Ah sorry about that, I added some more explanation. But basically, <sid>alternative_reminder('vsplit') is an expression whose value is the one returned at the end of the function. By default, a function returns the number 0. But here the function returns an empty string. So, the function doesn't add anything to the expansion of vsplit. However, to get to the returned value (an empty string), Vim must process the code of the function. The latter contains some logic to trigger a warning message when necessary.
    – user852573
    Oct 12, 2017 at 11:02
  • @Rich to better understand, try this command: fu! Func() ^@ return 'world' ^@ endfu | echo 'hello '.Func() But do not type ^@ literally, instead press C-v then C-j.
    – user852573
    Oct 12, 2017 at 11:04
  • 1
    Oh duh. Of course the function is evaluated before the <expr> abbrev returns: it's an expression, not a string. Thanks for explaining!
    – Rich
    Oct 12, 2017 at 12:47
3

It's not an elegant solution, but the following might work for you:

cnoremap split<cr> echo "Use CTRL-W s!"<cr>
cnoremap vsplit<cr> echo "Use CTRL-W v!"<cr>

N.B. You might need to adapt this with the exact series of keystrokes you use e.g. :vs<cr>

This means that instead of splitting the window, the :split and vsplit commands will admonish you for not using the keyboard shortcuts.

:vsplit <...> will work as normal, though: as soon as you diverge from the mapping (e.g. by typing <space> after :vsplit), Vim will abandon it.

The downside is that while you are typing vsplit you will lose most of the visual feedback, because Vim is waiting to see whether or not you are typing the mapping or a different series of keystrokes with a matching prefix. This may or may not be an issue for you.

1
  • Thanks for the suggestion and taking the time. I upvoted your response but felt like @user852573 's response deserved to be the accepted one since it was more thorough.
    – lonetwin
    Oct 11, 2017 at 10:57

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.