I don't know how reliable it is, but I came up with this:
fu! s:cnorea(lhs, rhs) abort
let s:counter = get(s:, 'counter', 0) + 1
fu! s:cnorea_{s:counter}() abort closure
call timer_start(0, {
\ -> execute('if histget(":", -1) ==# '.string(a:rhs)
\ .'| call histdel(":", -1)
\ | endif
\ | call histadd(":", '.string(a:lhs).')')
\ })
return a:rhs
endfu
exe printf('cnorea <expr> %s
\ getcmdtype() ==# ":" && getcmdpos() ==# %d
\ ? <sid>cnorea_%d()
\ : %s'
\ , a:lhs, strlen(a:lhs)+1, s:counter, string(a:lhs))
endfu
call s:cnorea('cv', 'e %:r.cpp')
It seems to work as you want: it expands cv
into e %:r.cpp
, and logs cv
in the command-line history. However, it relies on a timer, so if you want to test it, you first need to check that :echo has('timers')
returns 1
.
You could define as many similar abbreviations as you want, by invoking s:cnorea()
, passing to it the {lhs}
and {rhs}
of your abbreviations. Examples:
call s:cnorea('abcd', 'echo "hello"')
call s:cnorea('xyz', 'echo "world"')
It works like this:
call s:cnorea('cv', 'e %:r.cpp')
Invokes s:cnorea()
, passing the strings 'cv'
and 'e %:r.cpp'
as arguments. The latter does 2 things:
- define the function
s:cnorea_1()
define the abbreviation:
cnorea <expr> cv getcmdtype() ==# ":" && getcmdpos() ==# 3 ? <SID>cnorea_1('cv', 'e %:r.cpp') : 'cv'
Like your original abbreviation, this one uses an expression as its {rhs}
. The expression tests whether you're on a regular Ex command line (getcmdtype() ==# ':'
), and whether you've typed cv
at the beginning of the line.
If any of these 2 conditions isn't satisfied, the expression returns cv
, which negates the expansion. Otherwise, it's expanded into the output of another function s:cnorea_1()
, which is defined like this:
fu! s:cnorea_1() abort closure
call timer_start(0, {
\ -> execute('if histget(":", -1) ==# '.string(a:rhs)
\ .'| call histdel(":", -1)
\ | endif
\ | call histadd(":", '.string(a:lhs).')')
\ })
return a:rhs
endfu
It returns your the expansion of your abbreviation, but before that, it calls a timer which in turn will call histdel()
to delete :e %:r.cpp
from the history, and histadd()
to add cv
in the history.
A timer allows you to defer the execution of a function or a funcref. From the perspective of the timer, the latter is called a callback. Here, the timer doesn't call a function, but a lambda expression, which is a special kind of funcref (see :h Funcref
).
The syntax of a lambda expression is as follows:
{ args -> expr }
Example:
:echo { x,y -> x+y }(1,2)
3
It returns 3
, because 3
is the image of (1,2)
under the function f: x,y -> x+y
.
The lambda expression used in the timer is:
{
\ -> execute('if histget(":", -1) ==# '.string(a:rhs)
\ .'| call histdel(":", -1)
\ | endif
\ | call histadd(":", '.string(a:lhs).')')
\ }
The purpose of this lambda expression/callback is to execute some Ex commands. But Ex commands are not expressions, so you can't write them directly. You have to wrap them inside the function execute()
, which, as any function, is an expression. It takes as argument an Ex command (or a sequence of Ex commands separated by bars) and returns its output.
It doesn't need any argument, so there's nothing before the arrow ->
. It's just a routine which will execute:
if histget(":", -1) ==# 'e %:r.cpp'
call histdel(":", -1)
endif
call histadd(":", 'cv')
This checks whether the last entry in the command-line history is e %:r.cpp
(it should be, it's just a precaution), removes it if this is the case, then adds cv
.
To summarize, when you execute :cv
at the beginning of the command-line, it should be expanded into :e %r.cpp
, but in the process, a timer is called to replace :e %:r.cpp
with :cv
in the history.
On my machine, although the callback is invoked after 0
ms, it's not instantaneous. Therefore, if you execute :cv
, Vim should expand it into :e %:r.cpp
, then replace it in the history. If on your machine, the callback is instantaneous, you should increase its waiting time, by replacing the 1st argument passed to timer_start()
with another arbitrary value (as an example, with 1
, for 1
ms).
Also, it's not completely reliable, because if you expand your abbreviation, without executing it immediately:
:cv
hit <space> to expand `cv`
:e %:r.cpp
during a few seconds, edit the command-line, maybe to add another command
hit <Enter>
... then the timer will be called too soon, and in your history, you'll get both entries:
:cv
:e %:r.cpp
Again, you could increase the waiting time (1000
ms, 5000
ms, ...), but it's a hack.
A few other notes to better explain the code.
The counter is needed to make the functions used by every abbreviation unique:
let s:counter = get(s:, 'counter', 0) + 1
It's evaluated inside the function name, through an element of syntax called curly braces name
(see :h curly-braces-names
):
fu! s:cnorea_{s:counter}() abort closure
" └─────────┤
" └ curly braces names
This means that the 1st time you call s:cnorea()
, it will define the function s:cnorea_1()
for your 1st abbreviation. The 2nd time, it will define the function s:cnorea_2()
for your 2nd abbreviation, and so on.
You don't need to pass the arguments from s:cnorea()
to s:cnorea_1()
, because of the closure
attribute (see :h closure
). Thanks to this, inside s:cnorea_1()
, you can refer to variables which were defined in the scope of the outer function (s:cnorea()
). So, even though a:lhs
and a:rhs
are in the scope of the arguments of s:cnorea()
, s:cnorea_1()
can still access them.
The abbreviation is installed by executing the output of a printf()
invocation. You don't need it, you could install it without printf()
:
But, printf()
allows you to separate the code from the expressions it contains. Compare:
exe ' cnorea <expr> '.a:lhs
\.' getcmdtype() ==# ":" && getcmdpos() ==# '.(strlen(a:lhs)+1)
\.' ? <sid>cnorea_'.s:counter.'()'
\.' : '.string(a:lhs)
vs:
exe printf('cnorea <expr> %s
\ getcmdtype() ==# ":" && getcmdpos() ==# %d
\ ? <sid>cnorea_%d()
\ : %s'
\ , a:lhs, strlen(a:lhs)+1, s:counter, string(a:lhs))
You can use whichever version you find more readable (if any).