Interesting question! 👍
Reading your attempts of code made me think of the order in which the operations are actually executed, which is:
- The
omap
motion or text object is executed and selected.
- The operator command (
y
, d
, g~
, etc.) is executed on that block of text.
It seems to me that some of your assumptions have the model backwards and assume that the operator command is somehow already "pending" when the omap
function is executed.
With that in mind, let's address your more specific questions one-by-one:
Question 1
But what if I wish to perform some function before or after? This does not restore the position (which is my purpose for trying to figure this out.)
If you think of the order, you'll see that the omap
function will run to completion first and then the operator will execute. To have something execute after the operator you need to have that action pending to only be processed after that action finished.
It turns out that you can do that using feedkeys()
, since it only adds keystrokes to the typeahead buffer and will only process them once it's done with the current action.
Since the second action will take place out of the context of the current function, you'll have to preserve the information (in this case the restore_position
) in some kind of global variable. You can use either g:
, s:
or b:
, whatever works best in your case. (In my case I'll use global ones for simplicity, but it should just work in all other cases as well.)
A first solution to this problem (a little bit naive):
function MyTest_F()
let g:restore_position = winsaveview()
normal! ggVG
call feedkeys(":call winrestview(g:restore_position)\<CR>", "n")
endfunction
onoremap F :<C-U>call MyTest_F()<CR>
This will work as you expect. yF
will yank the whole buffer, but leave the cursor in the same place. Also a nice test is g~F
to swap case of the whole buffer, again leaving the cursor and window view where they currently are.
One small shortcoming here is that yF
will not show you the 44 lines yanked
message, instead the :call winrestview(...)
command will be left in the command-line. You can clear the command-line by adding a "\<C-L>"
to the end of that feedkeys, but a better solution is to use a separate normal-mode mapping to execute the second part of the mapping, make that one <silent>
and feed the keys for that one instead. You can use the <Plug>
special key for that purpose.
So a better solution is:
function MyTest_F()
let g:restore_position = winsaveview()
normal! ggVG
call feedkeys("\<Plug>(MyTest_RestoreView)")
endfunction
onoremap F :<C-U>call MyTest_F()<CR>
nnoremap <silent> <Plug>(MyTest_RestoreView) :call winrestview(g:restore_position)<CR>
This should also preserve the message printed by the operator, whenever one is printed!
Question 2
How does one cancel an operator pending command, so one can "hi-jack" it?
Again, if you think of the order, first the omap
happens and selects a region and then the operator is executed on that region. So you can't really send a cancellation keystroke (like <Esc>
or <C-C>
) the the operator... But what you can do is send it an empty region, in which case it will do nothing, which in essence is the same as cancelling it.
So with this function:
function MyTest_H()
let restore_position = winsaveview()
normal! ggyG
call winrestview(restore_position)
endfun
onoremap H :<C-U>call MyTest_H()<CR>
Running dH
will yank the whole buffer and not delete anything. (In fact, with this specific function you'll even get the 44 lines yanked
message.)
It turns out you can even access v:register
from inside your omap
function, so you can even have "adH
yank the whole buffer into the "a
register, with:
function MyTest_H()
let restore_position = winsaveview()
silent execute "normal! gg\"".v:register."yG"
call winrestview(restore_position)
endfun
onoremap H :<C-U>call MyTest_H()<CR>
Note that hijacking an operator this way looks pretty hacky... Yeah it works, but is it really a good idea to do so? In any case, I guess the key here is that creating an empty motion is a simple way to cancel the original operation, which I guess is what you were trying to determine here.
Regarding the opfunc
attempt, setting opfunc
is not really applicable unless you're going to use g@
at some point, which you're not really doing here.
Question 3
Can one simply return a range within an omap
e.g. :1,$
to mean all lines. The best I could come up with is as follows, but I feel there must be a much simpler way.
Well, you need to have a visual selection at the end of your function (assuming you don't want the cursor position in any of the two extremities of the selection), so no I don't think there's a much easier way than this one.
You could use:
call setpos(".", startpos)
normal! v
call setpos(".", endpos)
But that's about the same thing, so probably not that much simpler...
Note that you can't have a winrestview()
there, it will undo your visual selection, so you'll be left with nothing. (See question #1 again for a way to handle restoring the view after the operator has finished.)
So the small simplification I have to suggest is:
function MyTest_H()
let startpos = [0,1,0,'off']
let endpos = [0,line("$"),0,'off']
call setpos(".", startpos)
normal! v
call setpos(".", endpos)
endfun
onoremap H :<C-U>call MyTest_H()<CR>
Question 4
Instead of trying to restore the position in the motion, I should be doing it after the operator. Is it possible to change the y
command to map to something like the code below?
Yes, it is possible to create a y
operator that will restore the cursor to its original position.
In order to do so, you set an opfunc
that will execute the yanking and then you call g@
to take a motion for it.
Here there's a question of order of operations again, since by the time your opfunc
starts to execute, your cursor will have already moved as a result of the motion or omap
captured by g@
. So you need to save your view before you execute g@
. Which means you'll have to save it and restore it in two different contexts, meaning again you'll need to use a global to preserve it between them.
Here's a way to make that work, creating a separate \y
(or <leader>y
) mapping that will preserve the cursor position after the yank:
function! YankRestore(type, ...) abort
if a:type == 'line'
normal! '[y']
else
normal! `[y`]
endif
call winrestview(g:restore_position)
endfunction
nnoremap <silent> <leader>y :let g:restore_position=winsaveview()<Bar>set opfunc=YankRestore<CR>g@
See :help :map-operator
for an example of writing an appropriate opfunc
.
You may also want to have your function handle visual mode and create a corresponding xnoremap
to handle <leader>y
the same way from visual mode. (You could even have it preserve the visual selection, by running gv
after the yank!)
Hopefully this advice has been useful and this answer will improve your understanding of how operators and omap
functions work!
feedkey
and the 1stnormal
does the trick for me (which leaves you with a 3 lines function)