From 37c670e68297264a5606ffd0e25d7fb4c585a379 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 15 Aug 2026 13:34:51 -0400 Subject: [PATCH] fix(marks): undo reverts a mark set after the change #41330 Problem: A named mark updated after a change is moved back (treated as the original mark) by undo: :1mark d :$ dw :2mark d " 'd is on line 2 :undo " 'd is back on line 1 The undo header snapshots `b_namedm` when the change is recorded, and `u_undoredo()` restores that snapshot indiscriminately. Solution: Update the pending header's snapshot when a mark is set explicitly. Marks that the change itself moved go through mark_adjust(), not setmark_pos(), so those are still reverted. Similar to 2546741d1b36 (for extmarks): an explicit set inside an undo block is confused with an edit-driven adjustment. But the extmarks case is dealing with mid-edit moves, whereas named/regular marks only need the stale snapshot dropped. --- runtime/doc/news.txt | 3 +++ runtime/doc/vim_diff.txt | 6 ++++++ src/nvim/mark.c | 3 +++ src/nvim/undo.c | 13 +++++++++++++ test/functional/editor/mark_spec.lua | 29 ++++++++++++++++++++++++++++ 5 files changed, 54 insertions(+) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 0514f11883..257d15a9dc 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -514,6 +514,9 @@ 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. ============================================================================== REMOVED FEATURES *news-removed* diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index ea57d83dc1..afcbd7a641 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -682,6 +682,12 @@ Mappings: - "#" followed by a digit doesn't stand for a function key at the start of the lhs of a mapping. +Marks: +- |undo| does not restore a mark that you moved (|m|, |:mark|) after the change + being undone; the mark shifts with the text it was moved to, the same as a + mark the change never touched. Vim restores its old (wrong) position. Marks + moved by the change itself are still restored. + Motion: - The |jumplist| avoids useless/phantom jumps. diff --git a/src/nvim/mark.c b/src/nvim/mark.c index 69c081b687..60d432a4bc 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -50,6 +50,7 @@ #include "nvim/tag.h" #include "nvim/textobject.h" #include "nvim/types_defs.h" +#include "nvim/undo.h" #include "nvim/vim_defs.h" // This file contains routines to maintain and manipulate marks. @@ -184,6 +185,8 @@ int setmark_pos(int c, pos_T *pos, int fnum, fmarkv_T *view_pt) if (ASCII_ISLOWER(c)) { i = c - 'a'; RESET_FMARK(buf->b_namedm + i, *pos, fnum, view); + // Moving a mark is not part of the pending change, so undo must not revert it. #5754 + u_update_named_mark(buf, i); do_markset_autocmd((char)c, pos, buf); return OK; } diff --git a/src/nvim/undo.c b/src/nvim/undo.c index 2db2afd1cb..09c0e42f18 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -3235,6 +3235,19 @@ void f_undotree(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) tv_dict_add_list(dict, S_LEN("entries"), u_eval_tree(buf, buf->b_u_oldhead)); } +/// Drops named mark `idx` from the pending undo snapshot (if the user moved that mark after the +/// change was recorded, undo must not put it back). +/// +/// The mark is then left to mark_adjust(), so undo shifts it with its text, like other marks not +/// touched by the change. +void u_update_named_mark(buf_T *buf, int idx) +{ + u_header_T *uhp = buf->b_u_curhead != NULL ? buf->b_u_curhead : buf->b_u_newhead; + if (uhp != NULL) { + uhp->uh_namedm[idx].mark.lnum = 0; + } +} + // Given the buffer, Return the undo header. If none is set, set one first. // NULL will be returned if e.g undolevels = -1 (undo disabled) u_header_T *u_force_get_undo_header(buf_T *buf) diff --git a/test/functional/editor/mark_spec.lua b/test/functional/editor/mark_spec.lua index 3a94c0d5c1..1cf8f847eb 100644 --- a/test/functional/editor/mark_spec.lua +++ b/test/functional/editor/mark_spec.lua @@ -41,6 +41,35 @@ describe('named marks', function() eq({ 4, 0 }, api.nvim_buf_get_mark(0, 'c')) end) + it('moved after a change follow their text through undo #5754', function() + command('edit ' .. file1) + command('1mark d') + command('$') + feed('dw') + command('2mark d') + eq({ 2, 0 }, api.nvim_buf_get_mark(0, 'd')) + command('undo') + eq({ 2, 0 }, api.nvim_buf_get_mark(0, 'd')) + command('redo') + eq({ 2, 0 }, api.nvim_buf_get_mark(0, 'd')) + + -- A mark on a line the change deletes is cleared by the change; undo restores it. + api.nvim_buf_set_mark(0, 'e', 2, 2, {}) + command('2delete') + eq({ 0, 2 }, api.nvim_buf_get_mark(0, 'e')) + command('undo') + eq({ 2, 2 }, api.nvim_buf_get_mark(0, 'e')) + eq('1test2', fn.getline(2)) + + -- A mark moved after a line-count change shifts with the text it was moved to. + command('2delete') + eq('1test3', fn.getline(2)) + command('2mark f') + command('undo') + eq({ 3, 0 }, api.nvim_buf_get_mark(0, 'f')) + eq('1test3', fn.getline(3)) + end) + it('errors when set out of range with :mark', function() command('edit ' .. file1) local err = pcall_err(n.exec_capture, '1000mark x')