From f04dc2fcfc70c5003c3d48e7f1722576a18e9ec8 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 29 Apr 2026 13:09:55 +0800 Subject: [PATCH] vim-patch:9.2.0408: Insert-mode edits can corrupt undo (#39492) Problem: A command in Insert mode can edit the current buffer, e.g., with setline(). That edit appends to the current undo block, but Insert mode does not know that the cursor line may need to be saved again before the next typed edit. If the next typed edit is a at the start of a line, it can join away the line that was changed by the command before Insert mode saves that updated line. The newest undo entry can then still refer to the joined-away line, so undo sees a range past the end of the buffer and fails with E438. Solution: If a command in Insert mode changes the buffer, set ins_need_undo so stop_arrow() refreshes Insstart. This lets the next edit properly decide whether a new undo entry is needed (Jaehwang Jung) closes: vim/vim#20087 AI-assisted: Codex https://github.com/vim/vim/commit/e47daed4423182968f64b80c3d7613f0a98a50d4 Co-authored-by: Jaehwang Jung --- src/nvim/edit.c | 46 ++++++++++++++++++++-------------- test/old/testdir/test_undo.vim | 28 +++++++++++++++++++++ 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/nvim/edit.c b/src/nvim/edit.c index fbc0b156de..c125d6c65c 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -946,25 +946,27 @@ static int insert_handle_key(InsertState *s) break; case K_PASTE_START: - paste_repeat(1); - goto check_pum; - case K_EVENT: // some event - state_handle_k_event(); - // If CTRL-G U was used apply it to the next typed key. - if (dont_sync_undo == kTrue) { - dont_sync_undo = kNone; - } - goto check_pum; - case K_COMMAND: // command - do_cmdline(NULL, getcmdkeycmd, NULL, 0); - goto check_pum; + case K_LUA: { + bufref_T save_curbuf; + const varnumber_T tick = buf_get_changedtick(curbuf); + set_bufref(&save_curbuf, curbuf); - case K_LUA: - map_execute_lua(false, false); + if (s->c == K_PASTE_START) { + paste_repeat(1); + } else if (s->c == K_EVENT) { + state_handle_k_event(); + // If CTRL-G U was used apply it to the next typed key. + if (dont_sync_undo == kTrue) { + dont_sync_undo = kNone; + } + } else if (s->c == K_COMMAND) { + do_cmdline(NULL, getcmdkeycmd, NULL, 0); + } else { + map_execute_lua(false, false); + } -check_pum: // nvim_select_popupmenu_item() can be called from the handling of // K_EVENT, K_COMMAND, or K_LUA. // TODO(bfredl): Not entirely sure this indirection is necessary @@ -983,12 +985,16 @@ check_pum: pum_want.active = false; } - if (curbuf->b_u_synced) { - // The K_EVENT, K_COMMAND, or K_LUA caused undo to be synced. - // Need to save the line for undo before inserting the next char. + if (curbuf->b_u_synced + || (bufref_valid(&save_curbuf) + && curbuf == save_curbuf.br_buf + && tick != buf_get_changedtick(curbuf))) { + // The K_EVENT, K_COMMAND, or K_LUA synced undo or changed this buffer. + // Save the cursor line before the next typed edit. ins_need_undo = true; } break; + } case K_HOME: // case K_KHOME: @@ -2196,7 +2202,9 @@ int stop_arrow(void) new_insert_skip = 2; } else if (ins_need_undo) { if (u_save_cursor() == OK) { - // A command or event may have moved the cursor after syncing undo. + // A command or event may have moved the cursor or edited the + // buffer. Update Insstart so that later edits can properly decide + // whether an extra undo entry is needed. Insstart = curwin->w_cursor; Insstart_textlen = (colnr_T)linetabsize_str(get_cursor_line_ptr()); ins_need_undo = false; diff --git a/test/old/testdir/test_undo.vim b/test/old/testdir/test_undo.vim index 0cf2ca4b37..21b6dcf970 100644 --- a/test/old/testdir/test_undo.vim +++ b/test/old/testdir/test_undo.vim @@ -938,4 +938,32 @@ func Test_undo_line_backspace_after_insert_cmd_cursor_movement() bwipe! endfunc +func Test_undo_line_backspace_after_insert_func_edit() + new + setlocal backspace=eol undolevels=100 + + let v:errmsg = '' + call feedkeys("i\" + \ .. "\call setline(2, 'abc')\" + \ .. "\\u", 'xt') + + call assert_equal('', v:errmsg) + call assert_equal([''], getline(1, '$')) + bwipe! +endfunc + +func Test_undo_line_backspace_after_insert_cmd_edit() + new + setlocal backspace=eol undolevels=100 + + let v:errmsg = '' + call feedkeys("i\" + \ .. "\s/.*/abc/\" + \ .. "\\u", 'xt') + + call assert_equal('', v:errmsg) + call assert_equal([''], getline(1, '$')) + bwipe! +endfunc + " vim: shiftwidth=2 sts=2 expandtab