diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 04e1178301..0606b6fc18 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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", + "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, diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt index a525dbe5d6..2d4f02c583 100644 --- a/runtime/doc/repeat.txt +++ b/runtime/doc/repeat.txt @@ -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 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 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, }) < diff --git a/runtime/doc/undo.txt b/runtime/doc/undo.txt index 60220c0271..bec018e7cb 100644 --- a/runtime/doc/undo.txt +++ b/runtime/doc/undo.txt @@ -14,7 +14,8 @@ The basics are explained in section |02.5| of the user manual. 1. Undo and redo commands *undo-commands* or *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. diff --git a/src/nvim/input.c b/src/nvim/input.c index 73c15e9522..61c94cfb86 100644 --- a/src/nvim/input.c +++ b/src/nvim/input.c @@ -724,14 +724,6 @@ void redo_append_char(int c) } } -// Append a number to the redo buffer. -void redo_append_num(int n) -{ - if (!block_redo) { - kv_printf(redobuff.cur.body, "%d", n); - } -} - /// Appends string `s` (must be typeahead encoding) to the stuff buffer. void stuffReadbuff(const char *s) FUNC_ATTR_NONNULL_ALL diff --git a/src/nvim/input_cmdatom.c b/src/nvim/input_cmdatom.c index e2ba43dada..dad8e55e01 100644 --- a/src/nvim/input_cmdatom.c +++ b/src/nvim/input_cmdatom.c @@ -73,11 +73,20 @@ static uint64_t atom_captures = 0; static bool atom_suppressed = false; /// Mapping edited the buffer, or its insert-session cascaded: cascades as one unit, incl. motions. static bool map_edit = false; -/// Ticks per command frame (CmdFrame.id). +/// Incremented per command frame (CmdFrame.id). static uint64_t frame_id = 0; +/// The executing command's frame; its `parent` chain spans nested `normal_execute()`. +static CmdFrame *cur_frame = NULL; -/// Accumulating composite atom: an executing mapping/macro. See `vatom` for Visual composite. +/// The pending atom, while it spans CmdFrames (a mapping's commands, a stuffed continuation, an +/// operator awaiting its motion). See `vatom` for Visual composite. +/// +/// Also used by undo, to restore cursor position. static struct { + bool open; ///< True from the atom's first toplevel frame until it resolves. + bool stuffed; ///< Last frame had stuffed keys pending (next frame continues the atom). + bool redo; ///< The continuation completes the prepped redo ("!ip" stuffs ":.,.+1!"). + CmdOrigin origin; ///< State at the atom's start. CmdAtomVec atoms; ///< Subatoms of the mapping/macro. char *lhs; ///< Label: mapping LHS or macro "@x" (NULL: not collecting). bool queued; ///< A cascadable atom was queued (g_atoms) while collecting. @@ -86,12 +95,8 @@ static struct { bool macro; ///< Macro execution: captured as an "@x"-labeled atom. uint64_t frame; ///< CmdFrame already executing when a lookahead resolved this mapping ///< ("f(" + mapped key in one batch). 0: none. - CmdOrigin origin; ///< State at start. } composite; -/// The executing command's frame; its `parent` chain spans nested `normal_execute()`. -static CmdFrame *cur_frame = NULL; - /// State of a Visual composite atom. typedef enum { kVatomNone = 0, ///< No pending Visual atom. @@ -219,6 +224,25 @@ static int atom_origin_undoseq(CmdOrigin origin) return bufref_valid(&origin.buf) ? origin.buf.br_buf->b_u_seq_cur : 0; } +/// Cursor position from the pending atom's start, or else `w_cursor`. +pos_T atom_origin_pos(buf_T *buf) +{ + CmdOrigin o = composite.origin; + if (composite.open && o.buf.br_buf == buf && bufref_valid(&o.buf) + && buf_get_changedtick(buf) == o.tick) { + return o.pos; + } + return curwin->w_cursor; +} + +/// Decides the "origin" state. Usually `frame->origin`, except for "stuffed" cases. +/// A "stuffed" continuation belongs to its stuffing frame (`x` stuffs `dl`: the `dl` atom's origin +/// is at `x`). +static CmdOrigin frame_origin(const CmdFrame *frame) +{ + return frame->cont ? composite.origin : frame->origin; +} + /// Composes a CmdSpec into `redo_keys` format. /// @return Allocated key sequence. static char *atom_redo_keys(CmdSpec spec) @@ -521,7 +545,7 @@ bool atom_composite_active(void) return composite.lhs != NULL; } -/// Starts a composite: accumulate a mapping/macro's subatoms. +/// Starts collecting subatoms. static void atom_composite_start(const char *lhs, size_t len) { xfree(composite.lhs); @@ -529,7 +553,6 @@ static void atom_composite_start(const char *lhs, size_t len) composite.queued = false; composite.lossy = false; composite.frame = 0; - composite.origin = atom_origin(); } /// Emits the composite atom with its collected subatoms (`CmdAtom.atoms`). @@ -574,20 +597,23 @@ static void atom_composite_end(void) atom_free(&atom); } -/// Entering :terminal mode ends the composite. -void atom_term_enter(void) -{ - if (!mc_replaying()) { - atom_composite_end(); - } -} - -/// Discards the collecting composite (its subatoms): error/interrupt voided it. +/// Discards the pending atom and its subatoms: error/interrupt voided it (but the continuation +/// keys, typeahead/stuff, were flushed). void atom_composite_abort(void) { composite.macro = false; XFREE_CLEAR(composite.lhs); atoms_free(&composite.atoms); + composite.open = false; +} + +/// Entering :terminal mode resolves (ends) the pending atom. Terminal keys are never captured. +void atom_term_enter(void) +{ + if (!mc_replaying()) { + atom_composite_end(); + composite.open = false; + } } /// True if the just-executed command is user input. Excludes re-execution of captured @@ -796,8 +822,7 @@ void atom_typed_del(size_t len) kv_size(typed.keys) -= MIN(len, kv_size(typed.keys)); } -/// Forgets the redo-atom: new command, or a policy exclusion. -/// Only toplevel commands track it: a nested ":normal!" must not disturb it. +/// Discards the redo-atom: new or invalid command. Only at toplevel (not nested ":normal!"). static void atom_redo_reset(void) { if (!atom_is_user_cmd()) { @@ -992,7 +1017,7 @@ static bool atom_visual_end_suffix(char *suffix, const CmdSpec *spec, bool redoa }; if (spec != NULL) { kv_push(vatom.atoms, ((CmdAtom){ .type = kAOperator, .spec = *spec, .keys = suffix, - .origin = cur_frame->origin })); + .origin = frame_origin(cur_frame) })); } else { xfree(suffix); } @@ -1043,7 +1068,7 @@ void atom_capture_op(oparg_T *oap, cmdarg_T *cap, bool redo_yank) spec.cmd2 = operand ? NUL : cap->nchar; spec.cmdarg = operand ? cap->nchar : NUL; CmdAtom op_atom = atom_from_spec(kAOperator, spec); - op_atom.origin = cur_frame->origin; + op_atom.origin = frame_origin(cur_frame); atom_stage_set(&op_atom); } } else if (!Visual.active || oap->motion_force) { @@ -1098,7 +1123,8 @@ void atom_capture_op(oparg_T *oap, cmdarg_T *cap, bool redo_yank) XFREE_CLEAR(repeat_cmdline); } } else if (cap->cmdchar == K_LUA) { - redo_append_num(repeat_luaref); + char buf[NUMBUFLEN]; + redo_append_str(buf, snprintf(buf, sizeof(buf), "%d", repeat_luaref)); redo_append_str(S_LEN(NL_STR)); } } else if (Visual.active && redoable && oap->motion_force == NUL) { @@ -1143,7 +1169,7 @@ InsSession atom_ins_start(int cmd, long count, VisualIns vis, bool vblock) // A consumed selection opens the redo body, so the atom starts where the selection did. // Else the CmdFrame origin, from before the entry moved the cursor (a/A/…). .origin = vis == kVInsKeys ? vatom.origin - : cur_frame != NULL ? cur_frame->origin : atom_origin(), + : cur_frame != NULL ? frame_origin(cur_frame) : atom_origin(), }; if (vis != kVInsNone && !mc_replaying()) { if (vis == kVInsKeys && !(atom_visual_replayable() && (vatom.state & kVatomTyped))) { @@ -1197,11 +1223,19 @@ static void atom_ins_push(const InsSession *session, bool cascade) atom_push_raw(cascade, &atom); } -/// Samples the pre-command state at normal_execute() entry; atom_cmd_end() diffs against it to -/// classify the command (motion, Visual-mode transition, edit). Pushes the frame (`cur_frame`). +/// Toplevel entry: starts a new atom. Samples the pre-cmd state (`origin`); pushes the frame. void atom_cmd_start(CmdFrame *old) { + // A stuffed continuation frame is one cmd with its stuffing frame: `v2e".p` stuffs `c…`, `!ip` + // stuffs `:.,.+1!`. KeyStuffed=false if error/CTRL-C aborted, `composite.stuffed` is then stale. + old->cont = KeyStuffed && cur_frame == NULL && composite.open && composite.stuffed; old->origin = atom_origin(); + if (cur_frame == NULL && !composite.open) { + composite.open = true; + composite.stuffed = false; + composite.redo = false; + composite.origin = old->origin; + } old->visual = Visual; old->keytyped = KeyTyped; old->captures = atom_captures; @@ -1217,8 +1251,9 @@ void atom_cmd_start(CmdFrame *old) old->parent = cur_frame; cur_frame = old; curcmd.op_global = false; - // A stuffed continuation frame keeps the redo-prep. "!ipsort" spans both frames. - if (!(KeyStuffed && curcmd.redo_frame == old->id)) { + if (old->cont && composite.redo) { + curcmd.redo_frame = old->id; // The continuation completes the prepped redo. + } else { atom_redo_reset(); } } @@ -1278,7 +1313,7 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old) // Decided once, at session start. vatom.state = (old->keytyped || (atom_composite_active() && atom_is_user_cmd())) ? kVatomTyped : kVatomFed; - vatom.origin = old->origin; + vatom.origin = frame_origin(old); } // Decided by the session (not atom_capturable()), so fed selections (":normal! vjd") still // accumulate for redo-prep. Recording/replay commands are meta (not part of the edit). @@ -1305,11 +1340,6 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old) // // Capture: does this command own an atom? // - if (!stuff_empty() && curcmd.redo_frame == old->id && !Visual.active && !old->visual.active) { - // The stuffed continuation completes the redo (op_filter/do_bang()), and is the next frame - // (stuff precedes typeahead). Flushed instead? atom_cmd_start() checks KeyStuffed. - curcmd.redo_frame = old->id + 1; - } if ((vis && atom_captures == old->captures && ca->oap->op_type == OP_NOP) || (!Visual.active && !old->visual.active @@ -1352,7 +1382,7 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old) // did neither is a no-op (vim-surround "ysa[" whose surround char was ). bool effect = changed || reg_max_ts(true) > old->reg_ts; if (atom.keys != NULL && *atom.keys != NUL) { - atom.origin = old->origin; + atom.origin = frame_origin(old); atom_push(effect, &atom); } else { atom_free(&atom); @@ -1361,14 +1391,14 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old) && !(vis && unchanged)) { // Payload typed in the cmdline ("/pat"). Emit-only. Not if pattern was not found. CmdAtom atom = atom_from_cmdline(kAMotion, ca, ca->searchbuf); - atom.origin = old->origin; + atom.origin = frame_origin(old); atom_push(false, &atom); } else if (!vis && curcmd.cmdline != NULL && (ca->cmdchar == ':' || ca->cmdchar == K_COMMAND)) { // Same for ":cnext" or "cnext". Never a Visual subatom. CmdAtom atom = atom_from_cmdline(kAExcmd, ca, curcmd.cmdline); // Payload read during the cmdline execution (`ds)` getchar() => ")"). atom_payload_append(&atom, old); - atom.origin = old->origin; + atom.origin = frame_origin(old); atom_push(false, &atom); } else if (replayable && (!vis || (keycls & kKeyPayload) == 0)) { // Non-redoable command (u, zz, q=): never cascaded as an edit. @@ -1378,7 +1408,7 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old) spec.regname = 0; } CmdAtom atom = atom_from_spec(motion ? kAMotion : jump_cmd ? kAJump : kANormal, spec); - atom.origin = old->origin; + atom.origin = frame_origin(old); atom_push(follow, &atom); } else if ((scroll_cmd || mouse_cmd) && !atom_composite_active()) { // Emit-only (viewport-dependent). @@ -1388,7 +1418,7 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old) spec.count = 0; } CmdAtom atom = atom_from_spec(scroll_cmd ? kAScroll : kAMouse, spec); - atom.origin = old->origin; + atom.origin = frame_origin(old); atom_push(false, &atom); } if (vis && kv_size(vatom.atoms) == collected && !unchanged) { @@ -1423,11 +1453,20 @@ void atom_cmd_end(cmdarg_T *ca, CmdFrame *old) if (old->parent == NULL && !mc_replaying() && typebuf_typed() && stuff_empty()) { mc_clock_edge(map_edit); map_edit = false; - // Mapping contains its continuation. While op-pending, selection-active, or insert-will-resume - // (i_CTRL-O), composite keeps collecting: ",Dw" (":nnoremap ,D d") is one atom, `keys="dw"`. + // One atom spans its continuation: while op-pending, selection-active, or insert-will-resume + // (i_CTRL-O), it stays open. ",Dw" (":nnoremap ,D d") is one atom, `keys="dw"`. if (ca->oap->op_type == OP_NOP && !Visual.active && restart_edit == 0) { atom_composite_end(); + composite.open = false; // Resolved. } } + if (old->parent == NULL) { + // Stuffed keys pending: the next frame continues the atom. + composite.stuffed = !stuff_empty(); + // The continuation also completes the redo-prep. + // Except Visual: stuffing is captured in its payload. + composite.redo = composite.stuffed && curcmd.redo_frame == old->id + && !Visual.active && !old->visual.active; + } cur_frame = old->parent; } diff --git a/src/nvim/input_cmdatom.h b/src/nvim/input_cmdatom.h index 4bfb077dc2..71d20f8dbe 100644 --- a/src/nvim/input_cmdatom.h +++ b/src/nvim/input_cmdatom.h @@ -17,6 +17,7 @@ extern CmdAtomVec g_atoms; typedef struct CmdFrame CmdFrame; struct CmdFrame { CmdOrigin origin; ///< State at entry. + bool cont; ///< Stuffed continuation frame: the atom began earlier. VisualState visual; ///< Visual-mode state (active/start/mode are diffed). bool keytyped; ///< KeyTyped uint64_t captures; ///< Capture counter. diff --git a/src/nvim/undo.c b/src/nvim/undo.c index 25eba280f0..ff31fabca5 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -474,8 +474,9 @@ int u_savecommon(buf_T *buf, linenr_T top, linenr_T bot, linenr_T newbot, bool r uhp->uh_walk = 0; uhp->uh_entry = NULL; uhp->uh_getbot_entry = NULL; - uhp->uh_cursor = curwin->w_cursor; // save cursor pos. for undo - if (virtual_active(curwin) && curwin->w_cursor.coladd > 0) { + uhp->uh_cursor = atom_origin_pos(buf); // The pre-change position: undo restores it. + if (virtual_active(curwin) && curwin->w_cursor.coladd > 0 + && equalpos(uhp->uh_cursor, curwin->w_cursor)) { uhp->uh_cursor_vcol = getviscol(); } else { uhp->uh_cursor_vcol = -1; @@ -2530,31 +2531,40 @@ static void u_undoredo(bool undo, bool do_buf_event) curhead->uh_visual = visualinfo; } - // If the cursor is only off by one line, put it at the same position as - // before starting the change (for the "o" command). - // Otherwise the cursor should go to the first undone line. - if (curhead->uh_cursor.lnum + 1 == curwin->w_cursor.lnum - && curwin->w_cursor.lnum > 1) { - curwin->w_cursor.lnum--; - } - if (curwin->w_cursor.lnum <= curbuf->b_ml.ml_line_count) { - if (curhead->uh_cursor.lnum == curwin->w_cursor.lnum) { - curwin->w_cursor.col = curhead->uh_cursor.col; - if (virtual_active(curwin) && curhead->uh_cursor_vcol >= 0) { - coladvance(curwin, curhead->uh_cursor_vcol); - } else { - curwin->w_cursor.coladd = 0; - } - } else { - beginline(BL_SOL | BL_FIX); + if (undo && curhead->uh_cursor.lnum >= 1 + && curhead->uh_cursor.lnum <= curbuf->b_ml.ml_line_count) { + // Undo restores the pre-change text; restore the pre-change cursor too. #5989 + curwin->w_cursor = curhead->uh_cursor; + if (virtual_active(curwin) && curhead->uh_cursor_vcol >= 0) { + coladvance(curwin, curhead->uh_cursor_vcol); } } else { - // We get here with the current cursor line being past the end (eg - // after adding lines at the end of the file, and then undoing it). - // check_cursor() will move the cursor to the last line. Move it to - // the first column here. - curwin->w_cursor.col = 0; - curwin->w_cursor.coladd = 0; + // If the cursor is only off by one line, put it at the same position as + // before starting the change (for the "o" command). + // Otherwise the cursor should go to the first changed line. + if (curhead->uh_cursor.lnum + 1 == curwin->w_cursor.lnum + && curwin->w_cursor.lnum > 1) { + curwin->w_cursor.lnum--; + } + if (curwin->w_cursor.lnum <= curbuf->b_ml.ml_line_count) { + if (curhead->uh_cursor.lnum == curwin->w_cursor.lnum) { + curwin->w_cursor.col = curhead->uh_cursor.col; + if (virtual_active(curwin) && curhead->uh_cursor_vcol >= 0) { + coladvance(curwin, curhead->uh_cursor_vcol); + } else { + curwin->w_cursor.coladd = 0; + } + } else { + beginline(BL_SOL | BL_FIX); + } + } else { + // We get here with the current cursor line being past the end (eg + // after adding lines at the end of the file, and then undoing it). + // check_cursor() will move the cursor to the last line. Move it to + // the first column here. + curwin->w_cursor.col = 0; + curwin->w_cursor.coladd = 0; + } } // Make sure the cursor is on an existing line and column. diff --git a/test/functional/api/extmark_spec.lua b/test/functional/api/extmark_spec.lua index d7560305c3..8f5acda6a9 100644 --- a/test/functional/api/extmark_spec.lua +++ b/test/functional/api/extmark_spec.lua @@ -888,11 +888,12 @@ describe('API/extmarks', function() set_extmark(ns, marks[1], 1, 2) feed('0k>') check_undo_redo(ns, marks[1], 1, 2, 1, 6) - feed('j>') + -- "gg0": the cursor after undo/redo depends on |restore-undo-cursor|. + feed('gg0j>') expect('\t12345\n\t12345') check_undo_redo(ns, marks[1], 1, 6, 1, 3) - feed('j') + feed('gg0j') check_undo_redo(ns, marks[1], 1, 3, 1, 6) end) diff --git a/test/functional/editor/put_spec.lua b/test/functional/editor/put_spec.lua index 44c3a4f243..c7fda47f88 100644 --- a/test/functional/editor/put_spec.lua +++ b/test/functional/editor/put_spec.lua @@ -56,17 +56,9 @@ describe('put command', function() end local init_contents = curbuf_contents() local init_cursorpos = fn.getcurpos() - local assert_no_change = function(exception_table, after_undo) + local assert_no_change = function() expect(init_contents) - -- When putting the ". register forwards, undo doesn't move - -- the cursor back to where it was before. - -- This is because it uses the command character 'a' to - -- start the insert, and undo after that leaves the cursor - -- one place to the right (unless we were at the end of the - -- line when we pasted). - if not (exception_table.undo_position and after_undo) then - eq(init_cursorpos, fn.getcurpos()) - end + eq(init_cursorpos, fn.getcurpos()) end for _, test in pairs(test_variations) do @@ -77,13 +69,12 @@ describe('put command', function() local orig_dotstr = fn.getreg('.') t.ok(visual_marks_zero()) -- Make sure every test starts from the same conditions - assert_no_change(test.exception_table, false) + assert_no_change() local was_cli = test.test_action() test.test_assertions(test.exception_table, false) - -- Check that undo twice puts us back to the original conditions - -- (i.e. puts the cursor and text back to before) + -- Undo puts the cursor and text back to before the change. feed('u') - assert_no_change(test.exception_table, true) + assert_no_change() -- Should not have changed the ". register -- If we paste the ". register with a count we can't avoid @@ -104,9 +95,6 @@ describe('put command', function() return end - if test.exception_table.undo_position then - fn.setpos('.', init_cursorpos) - end if was_cli then feed('@:') else @@ -781,15 +769,13 @@ describe('put command', function() ) run_test_variations(select_down_test_defs) - -- Undo and redo of a visual block put leave the cursor in the top - -- left of the visual block area no matter where the cursor was - -- when it started. + -- "." repeat of an upward visual block put applies at the restored cursor, so the put text + -- lands elsewhere: skip the position check after redo. local undo_redo_no = map(function(table) local rettab = copy_def(table) if not rettab[4] then rettab[4] = {} end - rettab[4].undo_position = true rettab[4].redo_position = true return rettab end, normal_command_defs) @@ -809,20 +795,16 @@ describe('put command', function() ) describe('blockwise cursor after undo', function() - -- A bit of a hack of the reset above. - -- In the tests that selection direction doesn't matter, we - -- don't check the undo/redo position because it doesn't fit - -- the same pattern as everything else. - -- Here we fix this by directly checking the undo/redo position - -- in the test_assertions of our test definitions. + -- Undo and CTRL-R restore the pre-change cursor position, even for an upward selection + -- (the harness only covers "." redo, not CTRL-R). local function assertion_creator(_, _) return function(_, _) feed('u') -- Have to use feed('u') here to set curswant, because -- ex_undo() doesn't do that. - eq({ 0, 1, 1, 0, 1 }, fn.getcurpos()) + eq({ 0, 2, 1, 0, 1 }, fn.getcurpos()) feed('') - eq({ 0, 1, 1, 0, 1 }, fn.getcurpos()) + eq({ 0, 2, 1, 0, 1 }, fn.getcurpos()) end end diff --git a/test/functional/editor/undo_spec.lua b/test/functional/editor/undo_spec.lua index 74d41a4424..a317e2bb54 100644 --- a/test/functional/editor/undo_spec.lua +++ b/test/functional/editor/undo_spec.lua @@ -89,6 +89,31 @@ describe('u CTRL-R g- g+', function() undo_and_redo(4, 'g-', 'g+', '1') end) + it('u restores the pre-change cursor position #5989', function() + local api = n.api + local function undo_restores(row, col, keys, pre) + if pre then + command(pre) + end + api.nvim_buf_set_lines(0, 0, -1, true, { 'this is a test' }) + api.nvim_win_set_cursor(0, { row, col }) + feed(keys) + feed('u') + eq({ row, col }, api.nvim_win_get_cursor(0)) + end + undo_restores(1, 5, 'diw') -- Operator moves to the word start before deleting. + undo_restores(1, 5, 'd^') -- Backwards motion. + undo_restores(1, 5, 'atest') -- Insert entered after the cursor. + undo_restores(1, 5, 'viwd') + undo_restores(1, 2, ',x', 'nnoremap ,x wdiw') -- Mapping: where it was triggered. + -- i_CTRL-G_u breaks: each block restores to its own start. + api.nvim_win_set_cursor(0, { 1, 5 }) + feed('ifooubaru') + eq({ 1, 8 }, api.nvim_win_get_cursor(0)) + feed('u') + eq({ 1, 5 }, api.nvim_win_get_cursor(0)) + end) + describe('undo works correctly when writing in Insert mode', function() before_each(function() exec([[ diff --git a/test/functional/legacy/043_magic_settings_spec.lua b/test/functional/legacy/043_magic_settings_spec.lua index 4004b22cd9..aadd490418 100644 --- a/test/functional/legacy/043_magic_settings_spec.lua +++ b/test/functional/legacy/043_magic_settings_spec.lua @@ -37,7 +37,8 @@ describe('regexp with magic settings', function() feed('x:$') feed_command('set undolevels=100') feed('dv?bar?') - feed('Yup:') + -- Nvim: "k" after "u": undo restores the cursor to where "dv?bar?" started (the last line). + feed('Yukp:') feed_command('?^1?,$yank A') -- Put @a and clean empty line diff --git a/test/functional/legacy/prompt_buffer_spec.lua b/test/functional/legacy/prompt_buffer_spec.lua index ee7642f716..412bd6570f 100644 --- a/test/functional/legacy/prompt_buffer_spec.lua +++ b/test/functional/legacy/prompt_buffer_spec.lua @@ -543,10 +543,11 @@ describe('prompt buffer', function() {5:-- INSERT --} | ]]) feed('u') + -- Nvim: the cursor returns to its pre-"S" position (|restore-undo-cursor|). screen:expect([[ cmd: tests-initial | Command: "tests-initial" | - ^cmd: hello | + cmd: hell^o | {1:~ }| {3:[Prompt] }| other buffer | @@ -561,7 +562,7 @@ describe('prompt buffer', function() screen:expect([[ cmd: tests-initial | Command: "tests-initial" | - c^md > hello | + cmd > hell^o | {1:~ }| {3:[Prompt] }| other buffer | diff --git a/test/functional/lua/buffer_updates_spec.lua b/test/functional/lua/buffer_updates_spec.lua index 0910218c08..6b2ca56e94 100644 --- a/test/functional/lua/buffer_updates_spec.lua +++ b/test/functional/lua/buffer_updates_spec.lua @@ -167,6 +167,8 @@ describe('lua: nvim_buf_attach on_lines', function() tick = tick + 1 check_events { { 'test2', 'lines', 1, tick, 1, 1, 2, 0 } } + -- Undo restored the cursor (|restore-undo-cursor|); pin it for the relative edits below. + api.nvim_win_set_cursor(0, { 5, 0 }) feed('wix') tick = tick + 1 check_events { { 'test2', 'lines', 1, tick, 4, 5, 5, 16 } } diff --git a/test/functional/ui/inccommand_spec.lua b/test/functional/ui/inccommand_spec.lua index dabf54a177..b6eadbcfd3 100644 --- a/test/functional/ui/inccommand_spec.lua +++ b/test/functional/ui/inccommand_spec.lua @@ -639,7 +639,7 @@ describe(":substitute, 'inccommand' preserves undo", function() if case == 'split' then screen:expect([[ Inc substitution on | - two line^s | + ^two lines | | {1:~ }|*6 Already ...t change | @@ -647,7 +647,7 @@ describe(":substitute, 'inccommand' preserves undo", function() else screen:expect([[ Inc substitution on | - two line^s | + ^two lines | | {1:~ }|*6 Already ...t change | diff --git a/test/functional/ui/sign_spec.lua b/test/functional/ui/sign_spec.lua index 9a0c435573..8d64ecbd6a 100644 --- a/test/functional/ui/sign_spec.lua +++ b/test/functional/ui/sign_spec.lua @@ -554,9 +554,10 @@ describe('Signs', function() ]]) exec('norm 2Gdd') exec('silent undo') + -- Nvim: undo restores the cursor to where "2Gdd" started (|restore-undo-cursor|). screen:expect([[ - {7: }1 | - {7:S1}^2 | + {7: }^1 | + {7:S1}2 | {7: }3 | {7: }4 | {1:~ }|*9 diff --git a/test/old/testdir/test_search.vim b/test/old/testdir/test_search.vim index 4ec87ba0a6..65843bb708 100644 --- a/test/old/testdir/test_search.vim +++ b/test/old/testdir/test_search.vim @@ -767,8 +767,9 @@ func Test_search_regexp() call assert_equal([0, 2, 5, 0], getpos('.')) call assert_equal(2, line('$')) normal u - call assert_equal('9 foobar', getline('.')) - call assert_equal([0, 2, 6, 0], getpos('.')) + " Nvim: undo restores the cursor to where "dv?bar?" started. + call assert_equal('9 foobar', getline(2)) + call assert_equal([0, 3, 1, 0], getpos('.')) call assert_equal(3, line('$')) set undolevels&