patch 9.2.0408: Insert-mode <Cmd> edits can corrupt undo

Problem:  A <Cmd> 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 <BS> at the start of a line, it can join away
          the line that was changed by the <Cmd> 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 <Cmd> 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: #20087
AI-assisted: Codex

Signed-off-by: Jaehwang Jung <tomtomjhj@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Jaehwang Jung
2026-04-28 19:04:39 +00:00
committed by Christian Brabandt
parent 2d43240659
commit e47daed442
3 changed files with 45 additions and 4 deletions
+15 -4
View File
@@ -1132,6 +1132,10 @@ doESCkey:
case K_COMMAND: // <Cmd>command<CR>
case K_SCRIPT_COMMAND: // <ScriptCmd>command<CR>
{
bufref_T save_curbuf;
varnumber_T tick = CHANGEDTICK(curbuf);
set_bufref(&save_curbuf, curbuf);
do_cmdkey_command(c, 0);
#ifdef FEAT_TERMINAL
@@ -1139,10 +1143,15 @@ doESCkey:
// Started a terminal that gets the input, exit Insert mode.
goto doESCkey;
#endif
if (curbuf->b_u_synced)
// The command 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 != CHANGEDTICK(curbuf)))
{
// The command synced undo or changed this buffer.
// Save the cursor line before the next typed edit.
ins_need_undo = TRUE;
}
}
break;
@@ -2503,7 +2512,9 @@ stop_arrow(void)
{
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(ml_get_curline());
ins_need_undo = FALSE;
+28
View File
@@ -939,4 +939,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\<CR>"
\ .. "\<Cmd>call setline(2, 'abc')\<CR>"
\ .. "\<BS>\<Esc>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\<CR>"
\ .. "\<Cmd>s/.*/abc/\<CR>"
\ .. "\<BS>\<Esc>u", 'xt')
call assert_equal('', v:errmsg)
call assert_equal([''], getline(1, '$'))
bwipe!
endfunc
" vim: shiftwidth=2 sts=2 expandtab
+2
View File
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
408,
/**/
407,
/**/