Skip to content

fix: close the undo block without leaking buffer-local 'undolevels' into the global option - #12

Merged
teocns merged 2 commits into
teocns:mainfrom
boazy:fix/undolevels-global-leak
Sep 2, 2026
Merged

fix: close the undo block without leaking buffer-local 'undolevels' into the global option#12
teocns merged 2 commits into
teocns:mainfrom
boazy:fix/undolevels-global-leak

Conversation

@boazy

@boazy boazy commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Closes #11.

Accepting a suggestion could copy a buffer-local 'undolevels' value into the global option and silently disable undo in every buffer afterwards. This patch keeps the undo-block split and never writes the global option.

Root cause

do_accept() closes the current undo block with the documented vimscript idiom (:help undo-close-block):

vim.cmd("let &undolevels=&undolevels") -- one undo reverts the whole accept

'undolevels' is a global-local option. The unscoped &undolevels on both sides of the assignment reads the effective value and writes it back as the global value. The effective value is the buffer-local value whenever one is set.

A normal buffer can carry a persistent local undolevels of -1 or 0: common secure-style guards set it on COMMIT_EDITMSG, MERGE_MSG, /tmp/*, and *.bak at BufReadPre. Plugins such as diffview.nvim, mason.nvim, neogit, and nui.nvim also set it, but on nofile/nowrite panels that should_attach() never accepts. An accept in a normal buffer with such an override sets global undolevels to that value. Every buffer without a local override then has undo disabled: u reports "Already at oldest change", and undo trees render empty.

The fix

One token changes; both sides of the idiom are scoped to the global option:

-  vim.cmd("let &undolevels=&undolevels") -- one undo reverts the whole accept
+  vim.cmd("let &g:undolevels=&g:undolevels") -- one undo reverts the whole accept

Setting the option in any scope closes the undo block, so the split still happens. Only the value copy is gone.

Verification (Neovim 0.12.4)

Scenario Before After
Typed text, then accept, then u only the accept is reverted identical
Buffer-local undolevels=-1, then accept, then read global -1 (leak) unchanged
Buffer-local undolevels=5, then accept, then change global local value untouched (5)

test/flow_spec.lua gains a Round 6 regression case. It sets undolevels=-1 buffer-locally, accepts a suggestion, and asserts the global option is unchanged. Against the pre-fix code, the new assertion fails with got=-1, want=1000. With this patch, the full spec reports ALL PASS.

…nto the global option

'undolevels' is a global-local option, so the unscoped
`let &undolevels=&undolevels` idiom reads the effective (buffer-local when
set) value and writes it back as the GLOBAL value. Accepting a suggestion
in a buffer with a local override (-1/0 from diffview/mason/neogit/suda/
nui panels or tmp-file guards) silently disabled undo for every buffer
opened afterwards.

Scope both sides to &g:undolevels: the block-closing side effect triggers
on setting the option regardless of scope, so the split still works, and
no value is ever copied or pinned.

Fixes teocns#11
… global option

Round 6 sets undolevels=-1 buffer-locally (as diffview/mason/neogit-style
panel buffers do), accepts a suggestion, and asserts the global option is
unchanged, then drops the local override.

Against the pre-fix code the new assertion fails (got=-1, want=1000);
with the &g: fix the full spec reports ALL PASS.
@teocns
teocns merged commit be705bc into teocns:main Sep 2, 2026
1 check passed
@teocns

teocns commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Thank you for your contribution

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.

Accept copies buffer-local 'undolevels' into the global option, silently disabling undo

2 participants