(shrikant-sharat)
AboutLabsArchivesTagsAtom Feed

Vim undo breaks with auto-close plugins

Prelude

If you've used IDEs or other heavy editors ever in your life, you'd know how nice it is to have parentheses and brackets to get auto-closed. If you don't know what I'm talking about, its a feature usually present in IDEs like eclipse and easily recreated in vim with mappings like

inoremap ( ()<Left>

Of course, that's just a simple taste. There are vastly complicated plugins that achieve this.

Now, what's really super annoying about these plugins is that they tend to break vim's amazingly powerful undo functionality. In other words, if you are using an auto-close plugin, chances are, you can't rely on vim's undo anymore.

Debugging this and finding the cause has been on my todo list for quite some time and a few days ago, I finally sat down to explore. I am writing my experience here. First, a simple test case to see if the auto-close plugin you use breaks undo, open vim (a blank file) and hit the following keys:

iabc{<CR><ESC>u

Where instead of <CR> you'd hit the return key and instead of <ESC> you'd hit the Escape key. Decent knowledge of vim should tell you that after the above keys, you should end up with a blank file again. Right?

If instead, you see a closing brace dangling in the second line, your undo is broken. MUHAHAHAHAHA! You can't rely on undo anymore until you get rid of that one plugin!

What's going on?

So, experimenting with many auto-close plugins and reading the source of at least 3 of those, I say there are basically two different implementations of this functionality, which all these plugins use. The first one is pretty much what was shown at the start of this article,

inoremap ( ()<Left>
" or
inoremap ( <C-r>="()\<Left>"

I'm going to call this class of plugins, the critters. These do not break your undo. The next class of implementations, that do break your undo, the beasts, do a bit of dark sorcery with stuff like

inoremap ( <C-r>=MyAwesomePairInseter()<CR>

There is no dark sorcery here that is immediately apparent. The real sorcery is inside that function, where a call to setline() function is made to replace your current line to contain the parentheses text at the cursor. Doesn't make sense? Don't worry, you'll get it soon enough.

Which plugins? Name them!

Here are a few ones that break undo:

Beasts

  • https://github.com/vim-scripts/AutoClose
  • https://github.com/Raimondi/delimitMate
  • https://github.com/Townk/vim-autoclose

and these don't break undo

Critters

  • https://github.com/vim-scripts/ClosePairs
  • https://github.com/vim-scripts/simple-pairs
  • https://github.com/vim-scripts/Auto-Pairs

An initial look at them and you can tell, the ones that break undo are actually more popular and have a relatively larger code base. So why doesn't anyone complain about breaking undo? I think they do and I believe the root cause is a bug with vim itself.

The main difference in usability among these classes is again to do with undo. In the beasts, typing a brace does not start a new undo action, but it does in the critters (like hitting a <C-g>u). This might actually be playing a role in why undo breaks in beasts only, but the exact reason escapes me.

A reproducible test case

I wanted to reproduce this problem with a vanilla vim with no custom configuration (except for nocompatible). So, I checked out the latest version (vim73-353) from the mercurial repository, compiled (with python, ruby and usual shit) and opened it, with no plugins and a simple vimrc as the following:

set nocompatible

inoremap <buffer> <silent> ( <C-R>=<SID>InsertPair("(", ")")<CR>
inoremap <buffer> <silent> [ <C-R>=<SID>InsertPair("[", "]")<CR>
inoremap <buffer> <silent> { <C-R>=<SID>InsertPair("{", "}")<CR>

function! s:InsertPair(opener, closer)
    let l:save_ve = &ve
    set ve=all

    call s:InsertStringAtCursor(a:closer)

    exec "set ve=" . l:save_ve
    return a:opener
endfunction

function! s:InsertStringAtCursor(str)
    let l:line = getline('.')
    let l:column = col('.')-2

    if l:column < 0
        call setline('.', a:str . l:line)
    else
        call setline('.', l:line[:l:column] . a:str . l:line[l:column+1:])
    endif
endfunction

Which is a stripped down version of the auto-close functionality implemented in townk's auto-close plugin. And opened vim

vim -u undo-breaker-vimrc

and did the test here. Boom, a dangling brace character.

For all I know, its the call to setline() that's making all the difference. But I could be entirely wrong with that. I say this because that is the major difference between the two classes of implementations.

Next?

I use persistent-undo in vim73 and heavily depend on it. Combined with the gundo plugin by Steve Losh, I get a kind of nicely visualized version history that is centric to every file, which is quite handy in its own right.

So, if there are others who have faced this, have a fix for it, perhaps a patch to vim, or if there is already a bug in vim's bug database on this, let me know.

Thanks for reading.


Comments