In patch 8.0.1206, Vim has added 2 new events: CmdlineEnter
and CmdlineLeave
. They are fired every time you enter or leave a command line.
There are 7 types of command lines, including one for normal Ex commands, debug mode commands, search commands
(with a distinction between forward and backward), expressions, and inputs (more info at :h cmdwin-char
).
Each of them is associated with a symbol among this set : > / ? = @ -
.
You can limit the effect of an autocmd listening to CmdlineEnter
/ CmdlineLeave
to one or several types of command lines by:
- writing their symbols in the pattern field
- comparing the value of the expression
expand('<afile>')
to a regex matching several symbols, inside the executed command
au CmdlineEnter @ call feedkeys('my default input has ', 'in')
This autocmd populates the input field with the text my default input
.
After sourcing it, if you execute the following command, without modifying the default input:
echo strchars(input('')).' characters'
You should get the message:
my default input has 21 characters
The next autocmd populates the expression command line with the current
date, on the condition
that you leave it empty, and that the current line contains the word date
:
au CmdlineLeave = if getline('.') =~# 'date' && empty(getcmdline())
\| call feedkeys(strftime('%d-%m %H:%M'), 'in')
\| endif
I'm not sure it's a good idea to use this kind of code, as it may interfere with your config /
plugins, any time you have a mapping or abbreviation in insert mode which presses C-r =
. It would
be nice to be able to programmatically detect if C-r =
has been pressed during the expansion of a
mapping or an abbreviation, to limit the effect of the autocmd to a command line which has been
entered manually.
I guess you could manually set a flag in your config and test it in your autocmd, but it wouldn't be possible in third-party plugins.
CmdlineEnter
and CmdlineLeave
could be used in slightly more complex scenarios. Here are 5 of
them:
- fix a typo
- lazy load a plugin when the command line is used for the 1st time
- chain commands which are frequently used one after the other
- enable
'hlsearch'
after a search, and disable it after moving the cursor - display a message after an arbitrary command has been executed to remind you of something
When you copy a line of Vimscript in a buffer to put it on the command line, sometimes you may copy the newline at the end, which may be translated into a literal carriage return on the command line. If this happens, the command fails.
Another typo, which I make sometimes due to my keyboard layout, is to type a z
instead of an open
parenthesis. So, instead of executing :h matchstr()
, sometimes I execute :h matchstrz)
which
raises an error.
You could maybe fix these issues with abbreviations, using the <expr>
argument, or install a
wrapper mapping around <CR>
to inspect the command line before it's executed and change it if
needed:
cno <expr> <cr> <sid>ccr()
fu! s:ccr() abort
if getcmdline() =~# '\r$'
return "\<bs>\<cr>"
endif
return "\<cr>"
endfu
… but here's a solution using the new CmdlineLeave
event:
augroup my_cmdline
au!
au CmdlineLeave : if getcmdline() =~# '\r$'
\| call s:fix_typo('cr')
\| endif
au CmdlineLeave : if getcmdline() =~# '\v\C^h%[elp]\s+\S+z\)\s*$'
\| call s:fix_typo('z')
\| endif
augroup END
fu! s:fix_typo(label) abort
let cmdline = getcmdline()
let keys = {
\ 'cr': "\<bs>\<cr>",
\ 'z' : "\<bs>\<bs>()\<cr>",
\ }[a:label]
call timer_start(0, {-> feedkeys(':'.cmdline.keys, 'in')})
endfu
2 questions:
- a) Why the timer?
- b) Why saving the command line in a variable, instead of invoking
getcmdline()
directly inside the callback?
a) You can't send the keys right now, because the command hasn't been executed yet; from :h CmdlineLeave
:
Before leaving the command line.
But it seems you can't modify the command either. Maybe it's locked, I don't know. Anyway, you can re-execute a new (fixed) command with the timer.
b) When the callback will be processed, the old command line will be lost, so you must save it immediately.
Suppose you have a lot of abbreviations in command line mode, and you want to delay their
installation until you press :
for the 1st time. As an example, here's a function which installs
abbreviations to automatically uppercase some lowercase characters of the first word on the command
line when it matches the name of a custom command, so that you can type its full name in lowercase:
fu! s:cmdline_auto_uppercase() abort
let commands = getcompletion('[A-Z]?*', 'command')
for cmd in commands
let lcmd = tolower(cmd)
exe printf('
\ cnorea <expr> %s
\ getcmdtype() == '':'' && getcmdline() =~# ''\v^%(%(tab<Bar>vert%[ical])\s+)?%s$''
\ ? %s
\ : %s
\ ', lcmd, lcmd, string(cmd), string(tolower(cmd))
\ )
endfor
endfu
Depending on the number of plugins/custom commands you have, calling this function could install a
lot of abbreviations. To delay their installation until they are really needed (that is until the
command line is entered), you could temporarily remap :
and install an autocmd listening to
CursorHold, so that the 1st time you press :
, or &updatetime
ms has elapsed without you doing
anything, the function is called:
augroup my_lazy_loaded_cmdline
au!
au CursorHold * call s:cmdline_auto_uppercase() | au! my_lazy_loaded_cmdline
augroup END
nno <expr> : <sid>lazy_loaded_cmdline()
fu! s:lazy_loaded_cmdline() abort
sil! nunmap :
call s:cmdline_auto_uppercase()
return ':'
endfu
But with the CmdlineEnter
event, you can simply install one autocmd:
augroup my_lazy_loaded_cmdline
au!
au CmdlineEnter : call s:cmdline_auto_uppercase()
\| exe 'au! my_lazy_loaded_cmdline'
\| aug! my_lazy_loaded_cmdline
augroup END
It's a fire-once autocmd, meaning that it removes itself the first time it's executed.
Some commands are often executed consecutively. For example, after executing :ls
to list the
loaded buffers, you may execute :b
to give the focus to one of them, providing a number, or a part
of its name as an argument to the command.
To automate the process, you could install a wrapper mapping around <CR>
to inspect the
command line before it's executed, and react accordingly. The detail of this method is explained in
this gist.
It works very well, but using CmdlineLeave
gives a few benefits.
There's no need to implement all the code inside a single function, it can be split across several
files. Also, there's less risk of interference between your mapping and a plugin. For example, a
plugin may type <CR>
in a recursive mapping, assuming the user didn't remap this key. Because of
this, I had an issue with a plugin similar to vim-mucomplete.
Although, it's still possible that a conflict arises between 2 plugins competing to remap <CR>
.
Example: vim-slash vs vim-searchindex.
So, instead of remapping <CR>
, you could rewrite the previous gist using CmdlineLeave
:
augroup my_cmdline
au!
au CmdlineLeave : call s:cmdline_chain()
augroup END
fu! s:cmdline_chain() abort
let cmdline = getcmdline()
let pat2cmd = {
\ '(g|v).*(#|nu%[mber])' : [ '' , 0 ],
\ '(ls|files|buffers)!?' : [ 'b ' , 0 ],
\ 'chi%[story]' : [ 'sil col ' , 1 ],
\ 'lhi%[story]' : [ 'sil lol ' , 1 ],
\ 'marks' : [ 'norm! `' , 1 ],
\ 'old%[files]' : [ 'e #<' , 1 ],
\ 'undol%[ist]' : [ 'u ' , 1 ],
\ 'changes' : [ "norm! g;\<s-left>" , 1 ],
\ 'ju%[mps]' : [ "norm! \<c-o>\<s-left>" , 1 ],
\ }
for [pat, cmd ] in items(pat2cmd)
let [ keys, nomore ] = cmd
if cmdline =~# '\v\C^'.pat.'$'
if nomore
let more_save = &more | set nomore
call timer_start(0, {-> execute('set '.(more_save ? '' : 'no').'more')})
endif
return feedkeys(':'.keys, 'in')
endif
endfor
if cmdline =~# '\v\C^(dli|il)%[ist]\s+'
call feedkeys(':'.cmdline[0].'j '.split(cmdline, ' ')[1]."\<s-left>\<left>", 'in')
elseif cmdline =~# '\v\C^(cli|lli)'
call feedkeys(':sil '.repeat(cmdline[0], 2).' ', 'in')
endif
endfu
Inside the function, the dictionary pat2cmd
binds some patterns to some lists. Each pattern is
matched against the contents of the command line, and each list describes the next command to
automatically execute if there's a match. A list contains 2 items: a command and a boolean flag.
When the flag is on, it means that you want the previous command to be executed while 'more'
is
disabled. IOW, you don't want a long listing to be paused when it fills the screen, and Vim to
display the message -- More --
.
WARNING:
Both methods, remapping <CR>
or installing an autocmd listening to CmdlineLeave
may cause an issue
while in Ex mode (and debug mode?). As an example, consider this code written in a minimal vimrc
:
cno <expr> <cr> CCR()
fu! CCR()
return "\<cr>"
endfu
If you start Vim with just a minimum of initializations:
$ vim -Nu /tmp/vimrc /tmp/vimrc
Then press gQ
to enter Ex mode (while maintaining line editing commands and completion functions),
<CR>
to execute an empty command, then :vi
to get back to normal mode, you'll get the error E501
.
I don't know what it means, but I know it has something to do with the empty line inside the function.
The point is that both methods may interfere in an unexpected way.
Bottom line: don't write an empty line in a function processed when CmdlineLeave
is triggered.
If you want to see all the matches of the last search pattern highlighted after executing a search
command, until your cursor has moved, there are several ways of doing it. One way consists, again,
in remapping <CR>
:
cno <expr> <cr> <sid>wrap_cr()
nno <expr> <silent> n <sid>wrap_n(1)
nno <expr> <silent> N <sid>wrap_n(0)
nno <silent> <plug>(my_nohls) :<c-u>call <sid>set_nohls()<cr>
fu! s:wrap_cr() abort
if getcmdtype() =~# '[/?]'
call s:set_hls()
call feedkeys("\<plug>(my_nohls)", 'i')
endif
return "\<cr>"
endfu
fu! s:wrap_n(fwd) abort
call s:set_hls()
call feedkeys("\<plug>(my_nohls)", 'i')
return a:fwd ? 'n' : 'N'
endfu
fu! s:set_hls() abort
sil! au! my_search
set hls
endfu
fu! s:set_nohls() abort
augroup my_search
au!
au CursorMoved * set nohls | au! my_search
augroup END
endfu
And again, CmdlineLeave
is an alternative to this mapping:
augroup my_cmdline
au!
au CmdlineLeave /,\? call s:set_hls()
\| call timer_start(0, {-> s:set_nohls()})
augroup END
nno <expr> <silent> n <sid>wrap_n(1)
nno <expr> <silent> N <sid>wrap_n(0)
fu! s:wrap_n(fwd) abort
call s:set_hls()
call timer_start(0, {-> s:set_nohls()})
return a:fwd ? 'n' : 'N'
endfu
fu! s:set_hls() abort
sil! au! my_search
set hls
endfu
fu! s:set_nohls() abort
augroup my_search
au!
au CursorMoved * set nohls | au! my_search
augroup END
endfu
Suppose you want to display a message after some commands are executed, maybe to remind you that an
alternative exists for the command you've just typed. Without CmdlineLeave
, timer, lambda
expression, execute()
, you could achieve your goal with the following code:
let s:overlooked_commands = [
\ { 'old': 'vsplit', 'new': 'C-w v' },
\ { 'old': 'split' , 'new': 'C-w s' },
\ { 'old': 'q!' , 'new': 'ZQ' },
\ { 'old': 'x' , 'new': 'ZZ' },
\ ]
nno <silent> <plug>(my_reminder) :<c-u>call <sid>reminder(input(''))<cr>
fu! s:reminder(cmd) abort
redraw
echohl WarningMsg | echo '['.a:cmd.'] was equivalent' | echohl NONE
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.feedkeys("<plug>(my_reminder)".%s."<cr>")[1]
\ : %s
\ ', old, sold, sold, string(cmd.new), sold
\ )
endfor
endfu
call s:remember(s:overlooked_commands)
unlet! s:overlooked_commands
To use it, you would need to populate the list of dictionaries s:overlooked_commands
with the old
commands you're currently using, and their new counterparts you forget. The old command must be the
literal command that you type (example: :vsplit
), while the new one can be any text you want to
read to remind you of an alternative (example: C-w v
).
Two remarks:
-
Do NOT give the
i
flag tofeedkeys()
. You want to feed your keys to the typeahead buffer AFTER the command (:sp
,:x
, …) has been executed. Withi
, you would feed them before theEnter
which executes the command. So, instead of doing this::vs Enter <plug>(future_reminder)C-w v Enter
You would do this:
:vs <plug>(future_reminder)C-w v Enter Enter
IOW, <plug>(…)
would be literally inserted on the command line, whereas you wanted it to be
typed in normal mode, to call a function. As a result, it would just be interpreted as a filename.
- No need to put a backslash in front of
<plug>
or<cr>
, because:cnorea
(like all abbreviations/mappings commands) automatically translates these special characters. However, you do need to use double quotes (you could also double the single quotes). Not to allow the translation,:cnorea
doesn't care about the kind of quotes you use (it probably translates these special characters in a table regardless of the quotes / backslash used) , but because you're inside a string surrounded by single quotes.
With a recent Vim version which supports CmdlineLeave
, the whole code could be rewritten like this:
let s:overlooked_commands = [
\ { 'old': 'vs\%[plit]', 'new': 'C-w v', 'regex': 1 },
\ { 'old': 'sp\%[lit]' , 'new': 'C-w s', 'regex': 1 },
\ { 'old': 'q!' , 'new': 'ZQ' , 'regex': 0 },
\ { 'old': 'x' , 'new': 'ZZ' , 'regex': 0 },
\ ]
fu! s:remember(list) abort
augroup remember_overlooked_commands
au!
for cmd in a:list
exe printf('
\ au CmdlineLeave :
\ if getcmdline() %s %s
\ | call timer_start(0, {-> execute("echohl WarningMsg
\ | echo %s
\ | echohl NONE", "")})
\ | endif
\ ', cmd.regex ? '=~#' : '==#',
\ string(cmd.regex ? '^'.cmd.old.'$' : cmd.old),
\ string('['.cmd.new .'] was equivalent')
\ )
endfor
augroup END
endfu
call s:remember(s:overlooked_commands)
unlet! s:overlooked_commands
This time, you are not limited to literal commands, you can use regexes too, provided that you set
the 3rd flag to 1
in the dictionary.
This means that you can display a message not only when a command matching a literal text has just been executed, but any command matching an arbitrary regex.
Helpful. Thanks.