I'm trying to make semi-automatic parentheses work using vim-arpeggio and ultisnips. Everything works fine except the undo story. What I have now is this:

Arpeggioinoremap () ()<c-r>=UltiSnips#ExpandSnippet()<cr>

in the .vimrc and

snippet () "paren" i

in my snippets file.

What this does is this: When I type () in very close succession, arpeggio will trigger its mapping. The mapping reinserts the same characters, then enters the expression register where it calls the ultisnips expand function for its side effect of replacing the () characters with the expanded snippet. I end up the the pointer between the parentheses.

I like this solution because it does not replace () with anything when typed at normal speed. It's really only the simultaneously typed parens that get interpreted specially.

What does not work: undoing all of the mapping in one go. When I hit u, I get back to the stage where () is already entered. I was unable to find out how to make undojoin work here. UltiSnips#ExpandSnippet appears to work only in insert mode, so I cannot simply exit into normal mode and call :undojoin | eval UltiSnips#ExpandSnippet(), at least so it appears.

EDIT: the problem is not related to the arpeggio plugin. the same necessity for a double undo appears with

inoremap () ()<c-r>=UltiSnips#ExpandSnippet()<cr>

EDIT 2: I mis-stated the problem above, sorry for that! What's actually the problem is this:

After pressing u the expansion is undone and the cursor remains between (). The next undo again reproduces () without change. I believe this is because Arpeggio and Ultisnips both set an undo mark so to speak. I would like to get rid of the need for undoing twice. The desired behavior is: press u once, get (). Press u again, undo the previous text edit that includes insertion of ().

context: I have several more parentheses pairs defined in a similar way, for instance ;' which on my keyboard amounts to hitting the single quotation mark and the key left of it together. This is then mapped to '' by Arpeggio and further expanded to '$1'$0 by Ultisnips. Pressing undo once here gives '' and pressing it again, gives ;' which is undesired.

  • 1
    After posting my answer below, I discovered that the breaking of the undo sequence was deliberately added later, by setting the value of &undolevels, in a PR from 2015. For anyone who may be interested, this behavior may be reverted by deleting the 4 lines containing &undolevels (lines 158,168,811,819) in snippet_manager.py.
    – husB
    Dec 21, 2022 at 15:47
  • Regarding your "EDIT 2": Ultisnips creates both undopoints, as seen in line 811 and line 819. Comment out one of these lines and you should be good!
    – husB
    Jan 8, 2023 at 8:31

2 Answers 2



UltiSnips#ExpandSnippet() actually breaks the undo sequence twice, once before expansion, and another after the expansion. More precisely, UltiSnips#ExpandSnippet() calls snippet_manager.expand(), which calls snippet_manager._try_expand(). Inspecting the code for _try_expand(), we see that it

  1. starts an undo sequence
  2. expands the snippet (via _do_snippet())
  3. starts another undo sequence.

A quick remedy

I recommend to comment out line 819 to remove the second undo sequence (step 3). Note that for the () snippet, removing either one of the two undo sequences will work. However, this is not the case for the if the characters differ. For example, if a qq snippet expands to '', then fooqqbar will become

  -undo point-
  -undo point-

If the second undo sequence was removed, a single undo would leave you with foo'', and not fooqq.


Now, in place of commenting out that line, one could create an option to enable/disable that behavior, and submit a PR on the repository!


Besides UltiSnips#ExpandSnippet(), there is also UltiSnips#Anon(). The latter expands a snippet that is defined on-the-fly. Importantly, unlike UltiSnips#ExpandSnippet(), UltiSnips#Anon() does not break the undo sequence.

Thus you could modify the Arpeggio mapping to

Arpeggioinoremap () <c-r>=UltiSnips#Anon('($1)$0')<cr>

in your vimrc. The lines in the snippets file can also be removed, unless you really wish to expand () manually.

  • I tried this out, and it works! However, it's not perfect. The undo sequence is fully unbroken, and when I edit a line including an expansion of () and then undo, the full line is gone. What I would prefer is to stop the undo right before expansion, ie. the undo undoes just the expansion and '...previous text on the line ()' remains. I realize that this is what I was complaining about, and in fact I mis-stated my original problem in the question; I've edited my question to correct this now. Jan 1, 2023 at 19:29
  • I wonder if the Anon method together with an extra undo break point might do it? Jan 1, 2023 at 19:37
  • here is a clunky version that appears to do what i want: Arpeggioinoremap () ()<c-g>u<bs><bs><c-r>=UltiSnips#Anon('($1)$0')<cr> Jan 1, 2023 at 19:51
  • @user3240588 that certainly does work. Actually the #Anon function supports optional arguments, namely the trigger, description, and expansion options, so this could be "tidied up" into Arpeggioinoremap () ()<c-g>u<c-r>=UltiSnips#Anon('($1)$0','()',"",'i')<cr>
    – husB
    Jan 8, 2023 at 8:40

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.