Skip to content

Conversation

@nullromo
Copy link

@nullromo nullromo commented May 7, 2025

Related to #2.

When a macro is recorded, text is yanked, commands are run, etc. update the contents of whatever registers changed.

Problems

Already solved, see discussion below

Buffer collision

If the user has a buffer named @a for some other reason, it will be modified. One way around this is to have this plugin keep a map of buffers that it has opened. For instance, we could build a table like

{
    a = { 1000, 1001, 1002 },
    b = { 1005, 1007 },
}

and that way we would know exactly which buffers to update. In this case, if register a changes, we need to update buffers 1000, 1001, and 1002. If register b changes, then we need to update buffers 1005 and 1007. Every time we open a new :RegisterEdit buffer, we have to add it to this table. Every time we close one of those buffers, we should remove it from the list.

No live update

This still doesn't handle the "the buffer would update in real-time (character-by-character) while recording the macro" feature from #2. I don't know if this is possible. Vim registers are only updated when the macro is finished being recorded. There is no autocommand event for generic keypresses. So it might not be possible. In theory you could register an autocommand on a whole bunch of different events, like CursorMoved and such, but I think there would be cases that are missed. Not 100% sure. But maybe that's better than nothing. At the very least, after the macro is done it would be updated properly.

TODO list

  • Update registers after macros are recorded.
  • Update registers after text is copied, deleted, or changed.
  • Change filetype of registereditor windows.
  • Use CmdlineLeave to update certain registers (:, /, and =).
  • Figure out how to update the small delete register (-).
  • Wipe the black hole register (_) when it is modified.
  • Use InsertLeave to update . register.
  • Use TextYankPost to update numbered registers (0-9).
  • Figure out what to do about filename registers (# and %).
  • Shrink/grow window when updating content (see fix window sizing issue when running :RegisterEdit from a very small window #11).
  • Figure out how to update the / register when * or # are pressed from normal mode.
  • Figure out what's going on with the undo sequence numbers when using u from the - register edit buffer.
  • Add FocusGained listener to update system clipboard registers.
  • Decide how to handle unsaved modifications that would be overwritten.

Not Possible

These would become possible if a RegisterChanged event existed.

  • Update selection registers (* and +).
    • Requires a system clipboard trigger which does not exist. We currently update on FocusGained as a decent approximation.
  • Update registers on g*, g#, gd, and gD
    • Traditional mappings do not catch these if there is delay between keypresses.

@tuurep
Copy link
Owner

tuurep commented May 9, 2025

I've been floating around one idea in my head:

We could set filetype=registereditor, which could then be checked for this case:

If the user has a buffer named @a for some other reason, it will be modified.

And also allow adding a file in ftplugin/ to customize things such as line wrap, number/nonumber, or whatever.

Seems good right?

Looks like it's ok and a common practice to set a completely custom-named filetype, for example in vim-dirvish:

https://github.com/justinmk/vim-dirvish/blob/5d1d1ef45c6161af14c7d3d3097057e8fb5ac257/autoload/dirvish.vim#L527

I will now test this branch :)

@tuurep
Copy link
Owner

tuurep commented May 9, 2025

It works great and seems worth it to me.

It would be great and more consistent if we could also update non-macro registers when their content has changed, such as updating the " register after a yank operation. This might not be in the scope of this PR, but if you have any idea please let me know.

Code I think is ok and can be easily improved, if e.g. we find a better way to match the correct buffers,

I think we should add the filetype=registereditor and that might change that logic a bit. What do you think?

@nullromo
Copy link
Author

nullromo commented May 9, 2025

We could set filetype=registereditor

Great idea! Much simpler than keeping that map, and also can help solve some other problems as well going forward.

update non-macro registers when their content has changed

Also a great idea. We can use this PR. I'll work on implementing this using TextYankPost.

nullromo added a commit to nullromo/registereditor that referenced this pull request May 9, 2025
@nullromo
Copy link
Author

nullromo commented May 9, 2025

EDIT: disregard this comment. See below.

There is a problem with TextYankPost. It specifies

Cannot change the text. textlock

Textlock specifies that while you are doing TextYankPost, you cannot modify the buffer or switch to another buffer. I tried modifying the code like this

-M.update_register_buffers = function()
-   -- get the register that is being recorded
-   local register = vim.fn.reg_recording()
+M.update_register_buffers = function(yank)
+   -- get the register that is being recorded or yanked into
+   local register = yank and vim.api.nvim_get_vvar("event").regname
+       or vim.fn.reg_recording()
    local register = yank and vim.api.nvim_get_vvar("event").regname
        or vim.fn.reg_recording()
    -- get a list of all buffers
    local all_buffers = vim.api.nvim_list_bufs()
    -- iterate over all buffers, updating the matching ones
    for _, buffer in pairs(all_buffers) do
        -- get info about the buffer
        local buffer_name = vim.api.nvim_buf_get_name(buffer)
        local buffer_filetype =
            vim.api.nvim_get_option_value("filetype", { buf = buffer })
        -- if the buffer has the 'registereditor' filetype and is named
        -- @<register>, then it should be updated
        if
            buffer_filetype == "registereditor"
            and buffer_name:endswith("@" .. register)
        then
            -- get the content of the register
            local reg_content = vim.api.nvim_get_vvar("event").regcontents
-           local buf_lines = reg_content:split("\n")
+           local buf_lines = yank and reg_content or reg_content:split("\n")
            -- update the buffer with the register contents
            vim.api.nvim_buf_set_lines(buffer, 0, -1, false, buf_lines)
        end
    end
end
    -- update open RegisterEdit buffers when a macro is recorded
     vim.api.nvim_create_autocmd({ "RecordingLeave" }, {
-        callback = internals.update_register_buffers,
+        callback = function()
+            internals.update_register_buffers(false)
+        end,
     })

+    -- update open RegisterEdit buffers when text is yanked into a register
+    vim.api.nvim_create_autocmd({ "TextYankPost" }, {
+        callback = function()
+            internals.update_register_buffers(true)
+        end,
+    })

But it gave an error on the nvim_buf_set_lines call at the end: E565: Not allowed to change text or change window.

@nullromo
Copy link
Author

nullromo commented May 9, 2025

Oh, no worries I forgot about vim.schedule. 🙃

nullromo added a commit to nullromo/registereditor that referenced this pull request May 9, 2025
@nullromo
Copy link
Author

nullromo commented May 9, 2025

such as updating the " register after a yank operation

I think this is the only outstanding issue. All registers except the " register will be updated now when text is deleted or yanked.

Examples

  • :RegisterEdit c followed by "cyy will update the window.
  • :RegisterEdit " followed by yy will NOT update the window.
  • :RegisterEdit " followed by ""yy will update the window.

It's a shame that TextYankPost does not trigger on the " register...

TextYankPost -- Just after a |yank| or |deleting| command, but not if the black hole register |quote_| is used nor for |setreg()|.

@tuurep
Copy link
Owner

tuurep commented May 10, 2025

Looking at this now, I realize one additional problem:

When a register automatically updates like this, it sets modified on (shown by a [+] in the statusline), although in that case it should definitely be nomodified, i.e. the reg contents are saved

this is probably easy to fix

I'll take a look at the final "-register issue now

@tuurep
Copy link
Owner

tuurep commented May 10, 2025

Yeah this has some serious complications, because ideally we'd update the buffer whenever a register's contents has changed in any way, doesn't work either with for example:

:let @a = "foo"

when @a is open.

Then there are a few specialties like the @/ register updating when a / search has been entered.

So the difficult part is that if we can't do it all, where should we draw the line?

Gotta investigate if it's at all possible to have a generic: "if a register's contents has in any way changed, update visible buffers"

Edit: loose idea for the cases of :let @a = "foo" and /:

Edit: more registers that update in a more special way:

@tuurep
Copy link
Owner

tuurep commented May 10, 2025

Hmm, so about the ", is the issue just that we don't know that specifically the register " is targeted?

Maybe we could consider an approach:

  • on TextYankPost, update all visible registereditor buffers
    • implicitly, if there was nothing to update, appears like nothing happened
    • edit: probably needs some check if it did actually change, e.g. to set modified status correctly

or something similar?

Did a quick test that the TextYankPost event does still fire with no register specified, with

:autocmd TextYankPost * echo "Foo!"

@tuurep
Copy link
Owner

tuurep commented May 10, 2025

This was a bit brainstormy 😄 we don't have to do anything exactly as above but let me know if it gave you an idea

nullromo added a commit to nullromo/registereditor that referenced this pull request May 12, 2025
nullromo added a commit to nullromo/registereditor that referenced this pull request May 12, 2025
@nullromo nullromo force-pushed the update-registers branch from ec80739 to 8b498a5 Compare May 12, 2025 17:18
nullromo added a commit to nullromo/registereditor that referenced this pull request May 12, 2025
@nullromo
Copy link
Author

it should definitely be nomodified

Yes, missed that. Fixed.

TextYankPost does not trigger on the " register

I misread the docs on this one. What it actually is talking about is the black hole register, which is "_. As it turns out, the v:event key that I was looking at (regname) doc says "Requested register (e.g "x" for "xyy), or empty string for an unnamed operation." What was happening was that when you don't specify a register, this variable is blank. And when you don't specify a register, it will yank into the " register. So basically when the regname is blank, that means we should be updating the " register. So I made a change that addresses this.

That being said, we should probably disable :RegisterEdit _ because it's always going to be blank.

cases of :let @a = "foo" and /

Yes, I think CmdLineLeave is a good option to investigate here. We can loop over all the open registereditor buffers and update them.

more registers that update in a more special way

Yeah... some of these might be tricky. Need to look into it.

@tuurep
Copy link
Owner

tuurep commented May 12, 2025

Nice, very cool. The " register updating is working.

I now realized that using y/c/d in the registereditor buffer in " changes the content of itself 😄 this could be tricky..

Another edge case:

  • Undoing changes in registereditor doesn't change modified status appropriately (like it would on a normal buffer)
    • maybe unrelated to PR, didn't test yet

On "_ editing: I think the current behavior is actually sensible because just like when you do:

  • let @_ = "foo", it becomes empty
  • Saving changes in @_ buffer, it becomes empty

Putting out a message like "Blackhole register will always be empty" is a fine alternative too, but I find it harmless that it can be opened tbh, and then we don't even need a special message?

Yeah, the fact that CmdlineLeave takes care of a lot of regs that make sense to use with registereditor, makes this seem quite optimistic.

Some more ideas:

  • For . register, maybe InsertLeave would work?
  • Numbered registers @0-9, should probably be always updated on TextYankPost, if any are open?

@nullromo
Copy link
Author

y/c/d in the registereditor buffer in " changes the content of itself

Actually now that I think about it, this issue applies to all registereditor windows. If you modify the buffer and don't save it, then modify the underlying register, your changes are deleted. Vim has various ways of handling this (swap files, eww gross!) and that message we've all seen before:

W12: Warning: File "<filename>" has changed and the buffer was changed in Vim as well
See ":help W12" for more info.
[O]K, (L)oad File, Load File (a)nd Options: _

I suppose if the buffer is modified by the user and then the register updates, maybe we should issue a warning or keep the modified unsaved content or something. Or keep the current behavior of just blowing it away. What do you think?

Undoing changes in registereditor doesn't change modified status appropriately

It does for me.

Screen.Recording.2025-05-12.143010.mp4

On "_ editing

I'm not sure the current behavior is right. If you open :RegisterEdit _ and then modify it and save, then paste from the _ register, it's going to paste nothing. The buffer contents doesn't match the register contents after you save the buffer. Really the _ register is read-only so if we open it it should just be a read-only buffer. But it's also always empty, so there's no point in opening it. So pretty much if we make a special case for it, the special case should just be don't allow opening it at all since there's no point in doing so.

For . register, maybe InsertLeave would work?
Numbered registers @0-9, should probably be always updated on TextYankPost, if any are open?

Yes to both of these. Should look into it.

Note

I added a TODO list to the PR description.

@tuurep
Copy link
Owner

tuurep commented May 13, 2025

On file overwrite and undo:

I like the behavior where it changes without prompting (or as you say, "just blowing it away"), but only in the special case of " it's very jarring to try to edit that register's content's with regular vim operators 😄 (although possibly could happen with +, and numbered registers, at least, too)

It appears like you can easily get your last unsaved state back by undoing in the registereditor, but that's where I hit that modified not changing issue:

What I meant about undo not setting modified is this case:

  1. Open :RegisterEdit " with some contents
  2. Go to normal buffer and yiw somewhere
  3. @" buffer changes
    • You can however undo to go to the previous contents
    • But in the case of this undo, modified didn't change but it should

^ can you reproduce?

I'm not sure the current behavior is right. If you open :RegisterEdit _ and then modify it and save, then paste from the _ register, it's going to paste nothing.

Yeah although the way I see it that's precisely working as expected if we go by vim help description:

9. Black hole register "_				*quote_*
When writing to this register, nothing happens.  This can be used to delete
text without affecting the normal registers.  When reading from this register,
nothing is returned.

The buffer contents doesn't match the register contents after you save the buffer.

Actually maybe the closest to accurate would be to wipe out that buffer's contents on save? 😄

However yes, totally agreed it just makes no sense to edit that buffer. My favorite, kinda logically sound solution would be that wipeout on save, though. Do you see what I mean with this?

It's kinda like an Easter Egg though...

Thanks for the Todo, very nice

nullromo added a commit to nullromo/registereditor that referenced this pull request May 13, 2025
nullromo added a commit to nullromo/registereditor that referenced this pull request May 13, 2025
nullromo added a commit to nullromo/registereditor that referenced this pull request May 13, 2025
nullromo added a commit to nullromo/registereditor that referenced this pull request May 13, 2025
@nullromo nullromo force-pushed the update-registers branch from 64b795c to dd86730 Compare May 13, 2025 21:32
nullromo added a commit to nullromo/registereditor that referenced this pull request May 13, 2025
@nullromo
Copy link
Author

My latest update allows for updating the buffers automatically using CmdlineLeave. It works when you use :let @a = 'foo'. It works for the = register too. It also works for searching using /. However, if you modify the / register another way, like for example using * or # from normal mode, then the buffer will not update. There is no autocommand for searches in vim, so I'm not sure if there is a good way to keep the / register updated at all times... any ideas?

nullromo added a commit to nullromo/registereditor that referenced this pull request May 13, 2025
@tuurep
Copy link
Owner

tuurep commented Nov 21, 2025

Sounds great!

When that is out of the way, we are very close to the finish line. I'll open the issue,

btw it's ok to remove the whole test suite test-add-key-trigger too.

if you get what I'm saying

Yeah absolutely

P.S. I may want to refactor/rename some stuff in the end and squash some of the more WIP parts of the commit history

@tuurep
Copy link
Owner

tuurep commented Nov 22, 2025

This is really quite finished!

on the 2 failing tests: #12 (comment)

The @% I can reproduce like this:

  1. Use this keymap to open the reg
    • vim.keymap.set("n", "<leader>%", function() vim.cmd("RegisterEditor %") end)
  2. Right when opening, the content is not set

The @" macro recording test is like this:

  1. When ciw done inside the macro it sets @" contents as the deleted text, as it should
  2. But when recording ends, it refreshes again just fine, but not in the test

Should see if it happens for example on @a, when using an explicit "a yank in macro recording a...

Update:

Should see if it happens for example on @a, when using an explicit "a yank in macro recording a...

  1. It's only a problem for the " register
  2. Test would pass if the macro didn't delete/yank anything
  3. I've learned that even if you'd specify a register for the ciw, the " register updates in addition

Update:

Maybe relevant for the @% case:

:h W10

(Warning: Changing a readonly file)

But the test didn't pass when temporarily trying without the readonly property so maybe not.

The warning is pretty obtrusive on open of the readonly buffers anyways though. I'd like to make it quiet on open, and noisy only when trying to write.

Lack of restarts just happened to not be an issue for these tests, but
they were meant to restart.

Most extensive note on the most confusing failing test.
In addition, readonly buffers are set as `nomodifiable`, now they're:

    - nice to read, doesn't give "Warning: Changing a readonly file"
      errors when opening and initializing contents with a delay

    - clear that you cannot edit them: no matter what, those registers
      cannot be written to
@tuurep
Copy link
Owner

tuurep commented Nov 22, 2025

Patched @% bug with a pretty stupid ad-hoc but at least it works:

On open, literally set the buffer contents as { "@%" }

More important change: I made it so readonly buffers are also nonmodifiable (similar to helpfiles when you try to edit them: you won't be allowed)

And that they never show the error Warning: Changing a readonly file

Worst of all was that readonly regs contents came in with a delay due to this warning.

So, only 1 failing test left!

@tuurep
Copy link
Owner

tuurep commented Nov 22, 2025

Spotted a mistake, the whole time I've thought @# is readonly, but:

:h "#

This register is writable, mainly to allow for restoring it after a plugin has
changed it.  It accepts buffer number: >
    let altbuf = bufnr(@#)
    ...
    let @# = altbuf
It will give error |E86| if you pass buffer number and this buffer does not
exist.
It can also accept a match with an existing buffer name: >
    let @# = 'buffer_name'

Sigh. Todo: tomorrow

@tuurep
Copy link
Owner

tuurep commented Nov 22, 2025

@# behavior solved, pushing it soon once I confirm some things.

It's adding a little mess but something that should be rethought in the future anyways. Related: #19

I mistakenly thought @# is a readonly register. It can be written to,
but only if the content is a substring matching an existing buffername,
or an existing buffer id.

@# and @= need to update themselves immediately after write. This has
worked on :w, but only because:

    - you're on the cmdline, not in the buffer
    - CmdLineLeave is fired, refreshing all register buffers
@tuurep
Copy link
Owner

tuurep commented Nov 22, 2025

What in tarnation.

Now writing the @" buffer with :w wipes the buffer.

I'll make a separate test file for write behavior, getting it mixed up with autocmd autorefresh is making this harder.

@tuurep
Copy link
Owner

tuurep commented Nov 23, 2025

Somewhere along the way introduced a bug that multi-line contents can't be written (the register becomes empty)

On any register, not just " (relieved this isn't another " reg special case)

@tuurep tuurep force-pushed the update-registers branch 3 times, most recently from 037570e to aeef019 Compare November 23, 2025 01:49
Too confusing when I'm testing autorefresh and what happens on write at
the same time.

Currently there is a big bug that multiline contents can't be written to
registers, the reg becomes empty instead.
@tuurep
Copy link
Owner

tuurep commented Nov 23, 2025

Big progress! I could not fix the multiline write bug before reworking the whole set_register, and turns out everything was much simpler than what I thought when I started on this plugin a couple years ago.

See commit message:

At first I thought this is a big breaking change, but it turns out the
'newline at the end' -semantics are already similar, if not exactly the
same as what I achieved with my hacky approach before, see:

:h setreg()

If {options} contains no register settings, then the default
is to use character mode unless {value} ends in a <NL> for
string {value} and linewise mode for list {value}. Blockwise
mode is never selected automatically.

Related and now easier to implement: #19

At first I thought this is a big breaking change, but it turns out the
'newline at the end' -semantics are already similar, if not exactly the
same as what I achieved with my hacky approach before, see:

    :h setreg()

    If {options} contains no register settings, then the default
    is to use character mode unless {value} ends in a <NL> for
    string {value} and linewise mode for list {value}. Blockwise
    mode is never selected automatically.

Related and now easier to implement: tuurep#19
@tuurep
Copy link
Owner

tuurep commented Nov 23, 2025

@nullromo I'm quite finished, could you run the tests and tell me what you see?

image

I can't make sense of that @" macro recording test. Changing the ciw to ""ciw (explicit copy to ") also made it pass.

But certainly doesn't seem like a real problem, just some test environment thing?

Edit: also do the system cb/selection tests ever fail for you? (any of the clipboard='something' tests)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-resize registereditor splits when their content updates from external register writing Buffer autorefresh when register is externally updated

2 participants