mirror of
https://github.com/neovim/neovim.git
synced 2026-09-03 04:43:48 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
<
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<CR>" 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 <Esc>).
|
||||
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<CR>"). 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<CR>" or "<Cmd>cnext<CR>". 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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -888,11 +888,12 @@ describe('API/extmarks', function()
|
||||
set_extmark(ns, marks[1], 1, 2)
|
||||
feed('0<c-v>k>')
|
||||
check_undo_redo(ns, marks[1], 1, 2, 1, 6)
|
||||
feed('<c-v>j>')
|
||||
-- "gg0": the cursor after undo/redo depends on |restore-undo-cursor|.
|
||||
feed('gg0<c-v>j>')
|
||||
expect('\t12345\n\t12345')
|
||||
check_undo_redo(ns, marks[1], 1, 6, 1, 3)
|
||||
|
||||
feed('<c-v>j<LT>')
|
||||
feed('gg0<c-v>j<LT>')
|
||||
check_undo_redo(ns, marks[1], 1, 3, 1, 6)
|
||||
end)
|
||||
|
||||
|
||||
@@ -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('<C-r>')
|
||||
eq({ 0, 1, 1, 0, 1 }, fn.getcurpos())
|
||||
eq({ 0, 2, 1, 0, 1 }, fn.getcurpos())
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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<Esc>') -- 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('ifoo<C-G>ubar<Esc>u')
|
||||
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([[
|
||||
|
||||
@@ -37,7 +37,8 @@ describe('regexp with magic settings', function()
|
||||
feed('x:$<cr>')
|
||||
feed_command('set undolevels=100')
|
||||
feed('dv?bar?<cr>')
|
||||
feed('Yup:<cr>')
|
||||
-- Nvim: "k" after "u": undo restores the cursor to where "dv?bar?" started (the last line).
|
||||
feed('Yukp:<cr>')
|
||||
feed_command('?^1?,$yank A')
|
||||
|
||||
-- Put @a and clean empty line
|
||||
|
||||
@@ -543,10 +543,11 @@ describe('prompt buffer', function()
|
||||
{5:-- INSERT --} |
|
||||
]])
|
||||
feed('<Esc>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 |
|
||||
|
||||
@@ -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 } }
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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&
|
||||
|
||||
Reference in New Issue
Block a user