1

Consider the following code snippet, which aims to provide y without moving the cursor.

function! Yank_fixed_cursor()
    let save_cursor = getcurpos()
    call feedkeys("y", 'nitx')
    call setpos('.', save_cursor)
endfunction
nnoremap y :call Yank_fixed_cursor()<cr>
nnoremap yr :echom "Another mapping"<cr>

I tested this (wrapped in a file minimal.vim) using either

nvim -u minimal.vim test.txt

or

vim -uN minimal.vim test.txt

where test.txt is pretty much any file. A couple of questions:

  • In both vim and nvim, the command y works as expected with one-character motions (e.g. y{, yG, y0), but it doesn't work with motions that take more characters (e.g. ygg, y5k). Why, and can this be fixed?

  • Without the yr mapping (or any other mapping that starts with y), the code does not work in either vim or nvim. Why?

  • what do you mean by "does not work in vim?" Just a guess but vim defaults to vi-compatible mode when using -u. Try vim -Nu minimal.vim – Mass Mar 29 at 12:45
  • Thanks, this answers the first question. :) I edited my post to reflect your comments. – Rastapopoulos Mar 29 at 12:56
2

Without the yr mapping (or any other mapping that starts with y), the code does not work. Why?

That's because you're calling feedkeys() with the 'x' option, which will also process the typeahead buffer. So you need the next keypress to be in that buffer in order to execute the full command.

For example, if you type yG, you need the G to be on the buffer at the time the function is called. Having another longer mapping defined with the same prefix ensures that Vim will wait for more keystrokes (up to 'timeoutlen', depending on 'timeout' being enabled). If the additional keystrokes don't match a mapping, the additional keystrokes will end up in the typeahead buffer, which is exactly what you wanted.

the command y works as expected with one-character motions (e.g. y{yGy0), but it doesn't work with motions that take more characters (e.g. yggy5k). Why, and can this be fixed?

That's because the full motion is not in the typeahead buffer at the time the function is called. Since yg doesn't match our longer dummy mapping yr, that means Vim will call the function with only g in the typeahead buffer, which means feedkeys will execute yg, which doesn't make sense...

You can work around that, to some extent, with a similar trick you used for the original prefix itself. You could define a mapping longer than yg that will make Vim wait for one more character after those two:

nnoremap ygr :echom "Yet another mapping."<CR>

Or, perhaps cleaner:

nnoremap y<Esc> <Nop>
nnoremap yg<Esc> <Nop>

Using <Esc> is cleaner than r and using <Nop> works similarly to <Esc> simply cancelling the command.

This will fix ygg or yg$, ygm, etc. But not other prefixes. If you consider counts, such as y5k, it gets really tricky to cover them all.

Is feedkeys robust? Should I use it for this purpose?

While feedkeys is a fine function and there are places where using it makes sense. However, there's a much better way to implement what you're trying to accomplish here: operator-pending mappings.

The way you use it is by setting 'operatorfunc' to point to a function you defined and then call the g@ command at the end of your mapping, which will keep waiting for a {motion} and then executes the set operator function when it gets the motion. That way you can manage to run some code (in your case, setpos() to restore the cursor) after the operation is executed.

Here's a very naive example:

function! Yank_fixed_cursor()
    let g:save_cursor = getcurpos()
    set operatorfunc=Yank_restore_cursor
    return 'g@'
endfunction
function! Yank_restore_cursor(type)
    if a:type == 'line'
        silent exe "normal! '[V']y"
    else
        silent exe "normal! `[v`]y"
    endif
    call setpos('.', g:save_cursor)
endfunction
nnoremap <expr> y Yank_fixed_cursor()

As you can see, the Yank_fixed_cursor function now sets up the operator by using a separate function, Yank_restore_cursor, to execute the yank and restore the cursor. It finally returns g@, which is used in an <expr> mapping to create an operator-pending mapping, which will wait for a motion and execute the operator function.

The Yank_restore_cursor gets from Vim more context about the motion, such as an argument indicating whether it was a linewise or character-wise motion and the markers [ and ] set to the block around the motion. We then use that to execute the yank operation. Finally, we can restore the cursor to the saved position.

Since the save and restore of the cursor happen in different functions, you'll need a global variable to preserve the information in between the two function calls. (If you're in a plug-in, you can use a s: variable, which leaves you on an isolated namespace and makes it harder for some other script to interfere with your global state.)

There are quite a few shortcomings with this naive implementation, you need a lot more to make it completely robust. Some of the items missing:

  • Using yy won't work, since y by itself is not a valid motion... You can fix that with an additional nmap yy y_ mapping, since _ is a valid motion corresponding to the double letter commands (yy, dd, etc.) You might also want to map Y with nmap Y y_. Note you need recursive mappings for those, to trigger your original y mapping.

  • You should handle yanking to a non-default register. You can look at v:register to execute your operation. Note that you'll get your register information in Yank_fixed_cursor, so that's also something you'll want to save in a global variable to later use in Yank_restore_cursor when you actually execute the operation.

  • Same for counts, for example 5yj. You can look at v:count in Yank_fixed_cursor to check whether there was a count. Since in this case you want to "translate" it into y5j (i.e. use the count on the motion), you can handle this by returning 'g@'.v:motion from Yank_fixed_cursor when a count is passed, since this will leave the count pending when the motion is expected, and will then apply to the motion.

  • You might want to map y in Visual mode as well. In many cases you can reuse the execution function Yank_restore_cursor between both operator-pending mode and Visual mode. You typically do that by taking a variable number of arguments and having the Visual mode mappings pass an extra argument to indicate it was called in that context. See the examples in :help :map-operator for more details.

As you can see, this can get quite complex very quickly. There are many corner cases and covering them all takes work. There is quite a bit of complexity involved, but Vim gives you the tools to cover these in order to emulate a command that looks and behaves same as a native command.

If this topic interests you, you might want to read "Grep Operator" from "Learn Vimscript the Hard Way", by Steve Losh.(You'll actually want to read the whole material, but pointing you at the relevant section so you'll see that's thoroughly covered here.)

| improve this answer | |
  • Thank you very much for such a thorough answer! I have a small question: shouldn't feedkeys work as expected without the x flag? :call feedkeys("y", "t") seems to work very well when called from the command line. – Rastapopoulos Mar 30 at 8:35
  • @Rastapopoulos Using feedkeys("y", "t") will leave a y "pending" and expecting a motion. But if you're planning to do something after the yank (such as restoring the cursor position), you won't be able to, since the function execution ends before the yank actually happens... Right? – filbranden Mar 30 at 16:27
  • 1
    Many thanks for this, I understand better now. – Rastapopoulos Mar 31 at 8:57

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.