18

I've been using the undofile feature in Vim for a while now. It's a very nice feature.

However, one annoyance is that it's very easy to accidentally undo changes that I did the last time I opened the file; which may be 2 minutes ago, an hour ago, last week, or a month ago.

For example, let's say I open a file, make a few changes, go off and change some other files, and discover that my changes weren't required, or perhaps they were just a few temporary debug statements.

Previously, I could just hold the u key until Vim said "Already at oldest change", :wq, and be done. But now I have to be very careful not to undo the changes I did last time I opened the file. There is no obvious way to see when you are doing this.

Is there any way to either make this more explicit? For example by showing it somewhere, issuing a warning, or even asking for a confirmation.

1
  • 2
    This is a question I've wondered since the unveiling of the undofile feature, and one I've been meaning to get around to asking since the unveiling of this site! Hoping there's a good answer. I think I'd also accept an answer that gave a command that reverted the file to the state it was in since the last time you opened it. (Or better, jumped back to that undo state.)
    – Rich
    Feb 23, 2015 at 11:27

5 Answers 5

8

I had this exact problem. Here's what I added to my vimrc to fix it for me:

" Always write undo history, but only read it on demand
" use <leader>u to load old undo info
" modified from example in :help undo-persistence

if has('persistent_undo')
  set undodir=~/.vim/undo " location to store undofiles
  nnoremap <leader>u :call ReadUndo()<CR>
  au BufWritePost * call WriteUndo()
endif

func! ReadUndo()
  let undofile = undofile(expand('%'))
  if filereadable(undofile)
    let undofile = escape(undofile,'% ')
    exec "rundo " . undofile
  endif
endfunc

func! WriteUndo()
  let undofile = escape(undofile(expand('%')),'% ')
  exec "wundo " . undofile
endfunc

Note that you don't set the undofile option; you need to remove in from your vimrc if you have it.

Undo now works as "normal". But we do manually write to the undo file with the wundo command in BufWritePost.

The ReadUndo() function manually reads the undo file with rundo and sets the undofile option. Now we can use u to go further back into history. I mapped this to <Leader>u.

This could probably be better. It overwrites the previous undo info, so you can't go back to the previous previous time you edited the file. But I haven't needed that myself.

2
  • Note I've noticed an issue with this: you only save the contents of the current session to the undo file. This is different from the default behaviour, where the contents of the previous session + all info already in the undo file is saved ... The way to fix this, would be to read the undo file, merge the undo changes, and then write the undo file ... But this doesn't seem possible ... :-/ May 2, 2015 at 21:39
  • @Carpetsmoker I agree. It would be nice to do that if it was possible. As it happens this has worked well enough for me for a long time. YMMV.
    – superjer
    May 8, 2015 at 23:38
4

I have the following, which stops and beeps when undo reaches the persisted buffer contents.

runtime autoload/repeat.vim " Must load the plugin now so that the plugin's mappings can be overridden.
let s:undoPosition = []
function! s:StopAtSavedPosition( action )
    " Buffers of type "nofile" and "nowrite" never are 'modified', so only do
    " the check for normal buffers representing files. (Otherwise, the warning
    " annoyingly happens on every undo.)
    if ingo#buffer#IsPersisted() && ! &l:modified && s:undoPosition != ingo#record#PositionAndLocation(1)
        " We've reached the undo position where the buffer contents correspond
        " to the persisted file. Stop and beep, and only continue when undo is
        " pressed again at the same position.
        call ingo#msg#WarningMsg(a:action . ' reached saved buffer state')
        execute "normal! \<C-\>\<C-n>\<Esc>" | " Beep.

        let s:undoPosition = ingo#record#PositionAndLocation(1)
        return 1
    else
        let s:undoPosition = []
        return 0
    endif
endfunction
nnoremap <silent> u     :<C-U>if ! &l:modifiable<Bar>execute 'normal! u'<Bar>elseif ! <SID>StopAtSavedPosition('Undo')<Bar>call repeat#wrap('u',v:count)<Bar>endif<CR>
nnoremap <silent> <C-R> :<C-U>if ! &l:modifiable<Bar>execute "normal! \<lt>C-R>"<Bar>elseif ! <SID>StopAtSavedPosition('Redo')<Bar>call repeat#wrap("\<Lt>C-R>",v:count)<Bar>endif<CR>

This integrates with the repeat.vim plugin; it requires my ingo-library plugin.

2
  • Are you using "persisted" to mean "the state the file was in when the buffer was loaded"? I'd usually expect "persisted" to mean the state of the file currently saved to disk.
    – Rich
    Mar 31, 2015 at 14:13
  • @Rich: No, we have the same definition. My mappings don't do exactly what the question asks for, but I still found it very useful. Mar 31, 2015 at 14:28
4

Oooh, ooh, finally a chance to show off this nifty command!

Vim can "go back in time." It has an :earlier command...

                                                        :ea :earlier            
:earlier {count}        Go to older text state {count} times.                   
:earlier {N}s           Go to older text state about {N} seconds before.        
:earlier {N}m           Go to older text state about {N} minutes before.        
:earlier {N}h           Go to older text state about {N} hours before.          
:earlier {N}d           Go to older text state about {N} days before.           

:earlier {N}f           Go to older text state {N} file writes before.          
                        When changes were made since the last write             
                        ":earlier 1f" will revert the text to the state when    
                        it was written.  Otherwise it will go to the write      
                        before that.                                            
                        When at the state of the first file write, or when      
                        the file was not written, ":earlier 1f" will go to      
                        before the first change.                                

... which can revert the file back to a previous state. This can be used in a number of ways.

  • If you can make a ballpark estimate of how long it took you to make these changes, you can feed a time into this command. Or, for example, if you know you hadn't changed the file (except for the changes you want to undo) in the past day, you can use

    :earlier 1d
    
  • If you've only written (saved) the file once since the changes you want to undo, or you haven't saved the file at all, you can use

    :earlier 1f
    

    As described in the :help text, this will revert to the previously written version if you just wrote the file, or revert to the last time it was saved if you don't have any saved changes.

This doesn't answer your question precisely, but it sounds like a bit of an XY problem.

6
  • 3
    it sounds like a bit of an XY problem: Why? I'm undo-ing with u, and I accidentally go past the changes I made right now ... I'm not sure what the original "X-problem" could be here? Feb 23, 2015 at 2:04
  • 3
    I now about :earlier by the way, but I still need to guess; just as I need to guess when using the u ... In cases it probably a bit better, but I would prefer something more explicit (if possible). Feb 23, 2015 at 2:05
  • 1
    @Carpetsmoker "X" is "I want to undo just these changes that I made most recently." "Y" is "How can I undo changes and ignore the undofile?" You sort of still have to guess, but you said in your question that the last changes you made were last week or earlier, so you could just do something like :ea 5d. You could also use the :ea 1f approach. In any case, it's much less granular.
    – Doorknob
    Feb 23, 2015 at 2:38
  • "X" and "Y" just seem a rephrasing of the same problem to me ? I did mention "weeks" in my question, but it could also be hours, or minutes (modified that) ... This is not a bad answer as such by the way, I was (and am) just hoping there is something better... Feb 23, 2015 at 2:57
  • 2
    I'm with @Carpetsmoker on this one. I've known about :earlier for some time, but I still have undofile switched off for exactly the reason described in the question.
    – Rich
    Feb 23, 2015 at 11:25
4

Update 2015-06-28: I fixed a small bug, and released this as a plugin. The plugin code is slightly better, in that it warns again after moving the cursor; I recommend you use the plugin.



The answer from superjer works great, but has the unfortunate side-effect that you can only undo changes from the last Vim session, and not all previous Vim sessions.

This is because wundo overwrites the undo file; it's not merged. As far as I know, there is no way to fix this.

So here's my alternative solution, it will show a big red warning message when you're undoing changes from the undo file.

This is similar to Ingo Karkat's answer, but it doesn't require an external plugin and has some subtle differences (displays warning instead of beep, doesn't require you to press u twice).

Note this only modifies the u and <C-r> binds, and not the U, :undo, and :redo commands.

" Use the undo file
set undofile

" When loading a file, store the curent undo sequence
augroup undo
    autocmd!
    autocmd BufReadPost,BufCreate,BufNewFile * let b:undo_saved = undotree()['seq_cur'] | let b:undo_warned = 0
augroup end 

" Remap the keys
nnoremap u :call Undo()<Cr>u
nnoremap <C-r> <C-r>:call Redo()<Cr>


fun! Undo()
    " Don't do anything if we can't modify the buffer or there's no filename
    if !&l:modifiable || expand('%') == '' | return | endif

    " Warn if the current undo sequence is lower (older) than whatever it was
    " when opening the file
    if !b:undo_warned && undotree()['seq_cur'] <= b:undo_saved
        let b:undo_warned = 1
        echohl ErrorMsg | echo 'WARNING! Using undofile!' | echohl None
        sleep 1
    endif
endfun

fun! Redo()
    " Don't do anything if we can't modify the buffer or there's no filename
    if !&l:modifiable || expand('%') == '' | return | endif

    " Reset the warning flag
    if &l:modifiable && b:undo_warned && undotree()['seq_cur'] >= b:undo_saved
        let b:undo_warned = 0
    endif
endfun
2

I think I'd also accept an answer that gave a command that reverted the file to the state it was in since the last time you opened it. (Or better, jumped back to that undo state.)

> Rich

I really liked the quoted idea so I made :Revert command. I hope you'll find it relevant to your question.

function! s:Real_seq(inner, outer) abort
  let node = a:outer
  for i in a:inner
    if has_key(i, 'alt')
      call s:Real_seq(i.alt, deepcopy(node))
    endif
    if has_key(i, 'curhead')
      return {'seq': node.seq}
    endif
    let node.seq  = i.seq
  endfor
endfunction

function! s:Get_seq(tree) abort
  let query = s:Real_seq(a:tree.entries, {'seq': 0})
  if (type(query) == 4)
    return query.seq
  else
    return undotree()['seq_cur']
  endif
endfunction

au BufReadPost,BufNewFile * if !exists('b:undofile_start') 
      \ | let b:undofile_start = s:Get_seq(undotree()) | endif

command! -bar Revert execute "undo " . get(b:, 'undofile_start', 0)

Helper functions Get_seq and Real_seq, based on undotree, are necessary because undotree()['seq_cur'] is sometimes not enough to pinpoint current position in undo tree. You can read more about it here.

1
  • this looks like a good solution. But you have an error in your au. the vimrc in there is the culprit. Simply leave it out
    – Naumann
    Jul 23, 2017 at 13:17

Your Answer

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

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