feat(editor): undo restores cursor position #41520

Problem:
Undo places the cursor wherever the cursor happened to sit at "save
time" (`uh_cursor` is sampled lazily on the first change).
Examples:
- `i` preserves, but `a` does not
- `diw`, `atest<Esc>`, `d^` abandon the original position
- `D`, `o` restore it (by accident).

Solution:
`composite` tracks the pending atom (and its `origin`) across frames.
A `stuffed` continuation frame inherits the `origin` + prepped redo.
Store `origin` info in the undo header, so undo can restore it.

- Not for a mid-command undo break (i_CTRL-G_u).
- Undoing a mapping restores where the mapping started (which
  technically may be different than where the "edit" started).
This commit is contained in:
Justin M. Keyes authored and GitHub committed 2026-08-27 16:12:11 -04:00
1 parent 617f9e628c
commit 7e2e3f8c25
16 files changed
+185 -137

No files matched your search

+5 -3
View File
@@ -305,6 +305,11 @@ EDITOR
real-time, instead of only on |FocusGained| or |:checktime|.
• During |complete()|-triggered completion, CTRL-N and CTRL-P are now subject
to insert-mode mappings.
• |undo| restores the cursor to its original position ("diw", "atest<Esc>",
"d^", …), instead of jumping to the start of the changed text.
• |undo| no longer restores the old (wrong) position of a mark that you moved
(|m|, |:mark|) later. The mark shifts with the text it was moved to, the
same as a mark the change never touched.
• Multibyte characters, translated by 'langmap', now invoke correct
mappings.
Example: >vim
@@ -526,9 +531,6 @@ These existing features changed their behavior.
`opts.plain=true` and now does not expand leading tildes ("~") in addition
to environment variables ("expand_env" is still accepted, for backwards
compatibility).
• |undo| no longer restores the old (wrong) position of a mark that you moved
(|m|, |:mark|) later. The mark shifts with the text it was moved to, the
same as a mark the change never touched.
• |:helptags| finds help tags with the "vimdoc" |treesitter| parser (and thus
requires it to be installed).
• `:helptags ALL` reports |E152| for "doc" directories it cannot write,
+9 -20
View File
@@ -103,11 +103,10 @@ operations provided by plugins, without the need for "announcement" via
vim-repeat or similar. >lua
local last ---@type vim.event.cmdatom.data?
local maxseq = {} ---@type table<integer, integer>
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
local is_redo_or_undo = ev.data.changed and (ev.data.undoseq or 0) <= (maxseq[ev.buf] or 0)
maxseq[ev.buf] = vim.fn.undotree(ev.buf).seq_last
local is_redo_or_undo = ev.data.changed and (ev.data.undoseq or 0) <= (vim.b[ev.buf].maxseq or 0)
vim.b[ev.buf].maxseq = vim.fn.undotree(ev.buf).seq_last
if ev.data.changed and not is_redo_or_undo and ev.data.lhs ~= '.' then
last = ev.data
end
@@ -168,27 +167,17 @@ without a count replays the macro. >lua
end)
<
*restore-undo-cursor*
Example: Restore cursor position after undo. Works for |u|, "3u", |CTRL-R|,
":undo N", |g-| and any mapping: >lua
By default, |undo| returns the cursor to its original position. If you don't
like that, you can use this snippet to get Vim's behavior instead: >lua
local seen = {} ---@type table<integer, table>
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
local seq = ev.data.undoseq
if not seq then
return
-- Undo/redo: the buffer changed to an already-seen undo state.
local undid = ev.data.changed and (ev.data.undoseq or 0) <= (vim.b[ev.buf].maxseq or 0)
vim.b[ev.buf].maxseq = vim.fn.undotree(ev.buf).seq_last
if undid then
vim.cmd('normal! `[') -- Start of the changed text.
end
local s = seen[ev.buf] or {}
seen[ev.buf] = s
-- Note: g- :earlier may cross undo-tree branches, "best effort" in that case.
if s.prev and seq < s.prev and s[seq + 1] then
vim.api.nvim_win_set_cursor(0, s[seq + 1]) -- Undo: first abandoned state.
elseif s.prev and seq > s.prev and s[seq] then
vim.api.nvim_win_set_cursor(0, s[seq]) -- Redo: revisiting a known seq.
elseif ev.data.changed and not s[seq] then
s[seq] = ev.data.pos -- New edit.
end
s.prev = seq
end,
})
<
+2 -1
View File
@@ -14,7 +14,8 @@ The basics are explained in section |02.5| of the user manual.
1. Undo and redo commands *undo-commands*
<Undo> or *undo* *<Undo>* *u*
u Undo [count] changes.
u Undo [count] changes. The cursor returns to its
original position.
*:u* *:un* *:undo*
:u[ndo] Undo one change.