This question is asked multiple places but none have an answer that works for me. I've tried: '' `` g; g, ; , Ctrl+o

In a file add:


Move your cursor over 7 in normal mode, type d4h, press u. You're back where you started but the cursor is on 3 now.

You can type 4l for this case to end up back where you were but what about more complex deletions.

  • for more complex deletions, set the mark first, then later go to that mark.
    – Maxim Kim
    Aug 26, 2021 at 12:20
  • If you already have a solution for your simple example, why don't you give an example of what you mean by "more complex deletions"?
    – romainl
    Oct 6, 2023 at 9:23

2 Answers 2


I don't think this is possible. When vim performs an undo, the cursor is left at the start of the change that is undone.

Here's an answer to a similar question which explains this.


Using lua api (nvim) we can do:

line, col = unpack(vim.api.nvim_win_get_cursor(0))
vim.cmd [[ normal 4X]]
vim.cmd [[ normal u]]
vim.api.nvim_win_set_cursor({0}, {line , col})

The first line saves line and column, the second line erases 4 chars, the third one gets back one action and the last one restores the line and cursor positions.

Your question helped me improve a function called "preserve":

function _G.preserve(cmd)
    local cmd = string.format('keepjumps keeppatterns execute %q', cmd)
    local line, col = unpack(vim.api.nvim_win_get_cursor(0))
    local lastline = vim.fn.line('$')
    if line > lastline then
        line = lastline
    vim.api.nvim_win_set_cursor({0}, {line , col})

The function win_get_cursor gives us more precise information. you could also create marks like @maxim-kim suggested.

As we can use this function to squeeze blank lines we are using an if statement to ensure the line will neve be beond the buffer last line.

Here a function to squeeze blank lines using the above function, in this case I placed the function at ~/.config/nvim/lua/tools.lua

local M = {}

M.squeeze_blank_lines = function()
    -- references: https://vi.stackexchange.com/posts/26304/revisions
    local old_query = vim.fn.getreg('/')    -- save search register
    preserve('sil! 1,.s/^\\n\\{2,}/\\r/gn') -- set current search count number
    local result = vim.fn.searchcount({maxcount = 1000, timeout = 500}).current
    local line, col = unpack(vim.api.nvim_win_get_cursor(0))
    preserve('sil! keepp keepj %s/^\\n\\{2,}/\\r/ge')
    preserve('sil! keepp keepj %s/\\v($\\n\\s*)+%$/\\r/e')
    if result > 0 then
        vim.api.nvim_win_set_cursor({0}, {(line - result), col})
    vim.fn.setreg('/', old_query)           -- restore search register

return M

Some more features that make use of the "preserve"

vim.cmd([[cnoreab cls Cls]])
vim.cmd([[command! Cls lua preserve('%s/\\s\\+$//ge')]])
vim.cmd([[command! Reindent lua preserve("sil keepj normal! gg=G")]])

Now you can use cls to remove trailing spaces, Reindent, and many other things you came up with.

-- keep cursor position when joinig lines
-- undo keeps cursor position (at least line)
local opts = { noremap=true, silent=true }

buf_set_keymap('n', 'J', ':lua preserve(":join")<cr>', opts)
buf_set_keymap('n', 'u', ':lua preserve(":undo")<cr>', opts)
buf_set_keymap('n', '<c-s>', [[:lua preserve("%s/\\s\\+$//ge")<cr>]])

With all these things your macros could become horrible, so I have an alias to open vim with not plugins or autocommands:

alias vinone="nvim -u NONE -U NONE -N -i NONE -c 'nnoremap <Space> /'"

I am mentioning this because sometimes we fix one thing and cause other problems. Read also about noautocmd

:sil! argdo :noautocmd norm! @a

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.