3

When writing functions to use in mappings and user-defined commands (for plug-ins), it's often convenient to use a Normal-mode command to yank or delete buffer contents. In those cases, it's also nice to save and restore register contents across the call, so that pasting from the affected registers works across the call. (In other words, using the register is an implementation detail and the user will be able to use P or p commands to paste register contents yanked/deleted before the function was executed.)

Some caveats of saving/restoring registers are quite widely known (for instance, it's important to store the register type with getregtype() and pass it back as the optional third argument to setreg() to preserve, for example, visual block yanks.)

But there are still some subtle issues with saving and restoring registers.

Take, for example, this simple function and mapping:

function! VisualContents() abort
    let savereg = getreg('"')
    let savetype = getregtype('"')
    normal! gvy
    let result = @"
    call setreg('"', savereg, savetype)
    return result
endfunction

xnoremap <Leader>e :<C-u>echo '['.VisualContents().']'<CR>

Generally, this does a good job of saving and restoring the register's contents across the call. If you yanked something with Y, yy or y in visual mode, or deleted something with a dd or similar, after this function call you'll be able to put it with P or p as if nothing affected the default register across this call.

However, if you yanked into a register other than "0 (the default register for yanks), this call will overwrite the contents of "0 with the register that was last used. For example, yank into register "z, with a command such as "zyy or "zy from visual mode, then invoke the <Leader>e mapping. Register "z will be intact across the call, but register "0 will be overwritten with the original contents of "z. The same is true if the operation preceding the <Leader>e call was a delete, in which case "0 will be overwritten with the contents of "1.

Note that the contents of the unnamed register will be unaffected (in a way), putting from the unnamed register should work fine, only putting from "0 explicitly will be broken.

One idea would be to save and restore register "0 across the call, but that will not really work. The problem is really that the unnamed register is a "pointer" and it's changing from whatever it was pointing earlier (such as "z) to the default yank register ("0), so restoring "0 will actually change what the contents of the unnamed register seems to be, which is definitely bad.

Another option would be to use a register other than the default "0 in the operation. For example, always use register "a using normal! gv"ay and saving/restoring register "a. But that has a similar problem, in that the unnamed register ends up "pointing" to register "a, which might (or will probably) differ from what it was pointing before the call, so putting from the unnamed register will not put the same contents as before the call, which is, again, not desirable.

One possibility would be to check which register the unnamed register points to (which is actually possible using getreginfo()) and then using that same register for the operation. But that seems to be quite heavy handed and complex, since it involves assembling the normal! command using an execute to use the custom register. It also involves at least a few awkward situations (such as potentially using the short-contents register "- for multiple lines, that seems to work, but is it the right thing to do?) or potentially invalid (is it possible that the unnamed register will be pointing to a read-only register? what about the expression register?)

What would be the most reliable way to save/restore registers across a call such as this one?

Is there a way to change what the unnamed register is "pointing" to without having to execute a yank/delete operation? (That would possibly help solve this issue, by restoring what the unnamed register points to after the operation.)

4
  • I know it's an example, but I wonder about using line/col on the marks '> and '<, along with some getline-type stuff to get the selection sans register? It's harder, but avoids the issue.
    – D. Ben Knoble
    Dec 10, 2020 at 3:22
  • 1
    @D.BenKnoble Yes often indeed it's best to use getline(), but it can be hard to cover all cases (even charwise is hard... blockwise too.) The motivation for this question was this previous answer (Mass left a comment pointing out saving/restoring the unnamed register was not fully reliable.)
    – filbranden
    Dec 10, 2020 at 3:35
  • 1
    Haven’t read either carefully enough to know if they’re duplicates, but this question is certainly closely related.
    – Rich
    Dec 18, 2020 at 23:11
  • Thanks @Rich! Indeed they're duplicates, I just marked them as such.
    – filbranden
    Dec 18, 2020 at 23:13

2 Answers 2

4

If your Vim includes the patch 8.2.0924, then you can use getreginfo(). Note that – since this patch – setreg() also accepts a dictionary as its second argument. The same kind of dictionary which getreginfo() returns, with all the necessary information to restore the unnamed register: that includes its contents, its type and the name of the register which it points to.

That's what I do in a library function which I use to yank a text-object in opfuncs.

Applied to your original example, it would give:

function! VisualContents() abort
    let savereg = getreginfo('"')
    normal! gvy
    let result = getreginfo('"').regcontents
    call setreg('"', savereg)
    return result
endfunction

xnoremap <Leader>e :<C-u>echo '[' .. VisualContents() .. ']'<CR>

Credit to @mass for submitting the patch 8.2.0924.


But I'm wondering if I need to save both getreginfo() for the unnamed register and also the one for "0 which is what gvy effectively writes to?

If the unnamed register was pointing to "0, then you shouldn't need to save the latter. Provided that setreg() is given all the info from getreginfo(), then it will restore the unnamed register by reconnecting it to "0 (thanks to the points_to key) and by restoring its contents into "0 (thanks to the regcontents key).

I think that was the main purpose of getreginfo():

Historically scripts often save/restore registers which is improved with getregtype, but that still destroys whatever pointer is set.

OTOH, if the unnamed register was pointing to another arbitrary register like "z, then you might indeed need to additionally save and restore "0:

function! VisualContents() abort
    let savereg_unnamed = getreginfo('"')
    let savereg_yank = getreginfo('0')
    normal! gvy
    let result = getreginfo('"').regcontents
    call setreg('"', savereg_unnamed)
    call setreg('0', savereg_yank)
    return result
endfunction

xnoremap <Leader>e :<C-u>echo '[' .. VisualContents() .. ']'<CR>
6
  • Oh wow, that's amazing! And it's been hiding below my nose this whole time... I haven't tried the code yet, but I got the part that the "points_to" part will be restored... But I'm wondering if I need to save both getreginfo() for the unnamed register and also the one for "0 which is what gvy effectively writes to? In any case, I'll take the code for a spin and report back. Thanks for the prompt answer!
    – filbranden
    Dec 10, 2020 at 0:09
  • 1
    But I'm wondering if I need to save both getreginfo() for the unnamed register and also the one for "0 which is what gvy effectively writes to? Ah no, you might need to save and restore "0 if the unnamed register was originally pointing to some other register. I'll make some tests and edit the answer later.
    – user938271
    Dec 10, 2020 at 0:53
  • 1
    Yes, you're right. I've just updated the answer and the code to restore "0 separately.
    – user938271
    Dec 10, 2020 at 1:03
  • 1
    @Mass The issue with gv"zy is that besides the "z register being used for the yank, the unnamed register will start pointing to the "z register. You can easily save/restore the "z register around that operation, but at the end, the unnamed register will keep pointing at "z, which is potentially (probably?) different from what it was pointing to. The next (unnamed) p or P will now put "z rather than what they would would have put before. getreginfo() + setreg(..., {"points_to": ...}) definitely makes it possible to address that! But it needs attention to detail.
    – filbranden
    Dec 10, 2020 at 3:00
  • 2
    @filbranden thank you for explaining it clearly and patiently. Perhaps in retrospect, this api should have been to save/restore all registers or such can be added to vim.
    – Mass
    Dec 10, 2020 at 3:07
2

I was never able to find the definitive answer to this problem either. However, as we all should somehow live with it, here are my two-penny worth two cents five kopeks:

  1. Do not use getreginfo(). It's "too new" and not backported to Neovim yet.

  2. Provide support for v:register in mappings whenever possible. Actually, it doesn't solve your particular "zero" problem completely, as in default scenario normal! gv""y will overwrite @0 and reset @@ anyway, but even your documentation of ["reg]<leader>e may give a hint that the mapping affects a register.

  3. Sometimes, while doing "yank" before "delete" (or "visual put" for the same reason), it's not a bad idea to clobber @9 being the least useful register of all.

  4. The value of @@ should not be overestimated. If nothing looks "bullet-proof" then just do it. That's not the end of the world if some "quote_number" is lost.

Not the answer you're looking for? Browse other questions tagged or ask your own question.