Undoing and redoing

Author

Marie-Hélène Burle

Undoing and redoing are operations so common while editing files that we don’t think about them much. Most software however have a poor undo/redo system in which edits get lost all the time.

Emacs’ undos never loses edits and undo-tree brings a wonderful undo/redo system to it.

Undo systems

Linear systems: classic undo/redo

You have some file:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px

You make some edit:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px

You make another edit:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px

And another one:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" "))---4((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px

You can undo:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" ")):::current---4((" "))
   classDef current stroke: #f96, stroke-width: 2px

You can undo some more:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" ")):::current---3((" "))---4((" "))
   classDef current stroke: #f96, stroke-width: 2px

You can also redo:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" ")):::current---4((" "))
   classDef current stroke: #f96, stroke-width: 2px

Now, you make some new edit. From this point on, some edits are lost:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" "))-.-4((" ")):::lost
   3((" "))---5((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px
   classDef lost stroke-dasharray: 3 4

You can still undo:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" ")):::current-.-4((" ")):::lost
   3((" "))---5((" "))
   classDef current stroke: #f96, stroke-width: 2px
   classDef lost stroke-dasharray: 3 4

And you can redo your last undo, but you can’t access all previous states of the file:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" "))-.-4((" ")):::lost
   3((" "))---5((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px
   classDef lost stroke-dasharray: 3 4

Linear systems: Emacs

You have some file:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((1)):::current
   classDef current stroke: #f96, stroke-width: 2px

You make some edit:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((1))---2((2)):::current
   classDef current stroke: #f96, stroke-width: 2px

You make another edit:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((1))---2((2))---3((3)):::current
   classDef current stroke: #f96, stroke-width: 2px

And another one:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((1))---2((2))---3((3))---4((4)):::current
   classDef current stroke: #f96, stroke-width: 2px

The first undo adds a new point to the chain of edits, reversing the effects of the last edit:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((1))---2((2))---3((3))---4((4))---5((3)):::current
   classDef current stroke: #f96, stroke-width: 2px

More undoing keeps adding points to the chain:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((1))---2((2))---3((3))---4((4))---5((3))---6((2)):::current
   classDef current stroke: #f96, stroke-width: 2px

There is no proper redo. Instead, you stop undoing, then start again to undo the undo:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((1))---2((2))---3((3))---4((4))---5((3))---6((2))---7((3)):::current
   classDef current stroke: #f96, stroke-width: 2px

You can make new edits

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((1))---2((2))---3((3))---4((4))---5((3))---6((2))---7((3))---8((5)):::current
   classDef current stroke: #f96, stroke-width: 2px

Nothing ever gets lost, but you might get headaches. For instance, to go back to the beginning, you have to do:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((1))---2((2))---3((3))---4((4))---5((3))---6((2))---7((3))---8((5))---9((3))---10((2))---11((3))---12((4))---13((3))---14((2))---15((1)):::current
   classDef current stroke: #f96, stroke-width: 2px

Non linear system: undo-tree

You have some file:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px

You make some edit:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px

You make another edit:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px

And another one:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" "))---4((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px

You can undo:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" ")):::current---4((" "))
   classDef current stroke: #f96, stroke-width: 2px

You can undo some more:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" ")):::current---3((" "))---4((" "))
   classDef current stroke: #f96, stroke-width: 2px

You can also redo:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" ")):::current---4((" "))
   classDef current stroke: #f96, stroke-width: 2px

Now, you make some new edit:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" "))---4((" "))
   3((" "))---5((" ")):::current
   classDef current stroke: #f96, stroke-width: 2px

You can still undo:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" "))---4((" "))
   3((" ")):::current---5((" "))
   classDef current stroke: #f96, stroke-width: 2px

You can switch branch and redo the old version:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" "))---2((" "))---3((" "))---4((" ")):::current
   3((" "))---5((" "))
   classDef current stroke: #f96, stroke-width: 2px

Nothing ever gets lost and it is a lot more sane to navigate the history.
To to back to the beginning, you only have to do:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((" ")):::current---2((" "))---3((" "))---4((" "))
   3((" "))---5((" "))
   classDef current stroke: #f96, stroke-width: 2px

Compare this with the insane Emacs default system:

%%{init: { 'flowchart': {'rankSpacing':20} } }%%

flowchart TD
   1((1))---2((2))---3((3))---4((4))---5((3))---6((2))---7((3))---8((5))---9((3))---10((2))---11((3))---12((4))---13((3))---14((2))---15((1)):::current
   classDef current stroke: #f96, stroke-width: 2px

And this is an exceedingly simple example only involving 5 different file states. I let you imagine how it quickly explodes in complexity in real life situations 🙂

Now, the default Emacs system has the huge benefit to never lose any edit. It is already a huge improvement over the default system on most software! The thing is that when we undo and redo changes, linear systems are not ideal. A tree structure that can be fully navigated is just a more sensible solution.

Undo-tree was initially developed for Vim, so Vim can also use an ideal undo/redo system.

Installing and customizing undo-tree

This is a personal affair.

The minimal configuration when using straight (to download the package) and use-package to load it and customize it, looks like this:

(use-package undo-tree
  :straight t)

My personal configuration looks like this:

(use-package undo-tree
    :straight t
    :init
    (global-undo-tree-mode 1)
    :bind (("C-l" . undo-tree-undo)
           ("C-r" . undo-tree-redo)
           ("s-t" . undo-tree-visualize)
           :map undo-tree-visualizer-mode-map
           ;; go to selected undo state
           ("<return>" . undo-tree-visualizer-quit)
           ;; cancel (return to state before calling undo-tree-visualize)
           ("q" . undo-tree-visualizer-abort)))