Open-Source Wikis

/

Neovim

/

Systems

/

Undo

neovim/neovim

Undo

Purpose

Vim's undo is a tree, not a stack: each change creates a node, and u walks the timeline backwards while <C-r> walks forward. Branching happens whenever you make a new change after an undo — the old future is preserved on a sibling branch and reachable via g+ / g- and :undolist. Neovim inherits this design and extends it to integrate with extmarks and the API.

Directory layout

src/nvim/
├── undo.c (~97k bytes)        The whole subsystem: tree, save/load, traversal
├── undo.h, undo_defs.h
├── ex_undo.c                  :undo / :redo Ex commands (small)
└── change.c (~76k bytes)      u_save / u_savesub / u_savedel — record a change

Key abstractions

Type / function File Description
u_header_T undo_defs.h An undo node: timestamp, sequence number, parent, children, list of u_entry_T.
u_entry_T undo_defs.h One contiguous range of changed lines: old content + cursor + line numbers.
u_save, u_savesub, u_savedel change.c "Mark this range as about to change" — must be called before mutation.
u_sync undo.c Close the current undo block. Called between insert-mode commands, on mode change, and when the editor is otherwise idle.
undo_read, undo_write undo.c Persist the tree to 'undodir'/'undofile'.
ex_undo, ex_redo ex_undo.c :undo / :redo Ex command handlers.

How it works

A buffer's undo tree lives at buf_T->b_u_oldhead, b_u_newhead, and b_u_curhead. Each node represents a "change set" — a group of edits that count as one logical operation for undo purposes. The grouping rule is roughly: each Vim command that modifies the buffer in Normal mode is its own block, and an Insert-mode session from i to <Esc> is one block.

Recording a change

Anything that changes a line must call u_save_cursor() (or its variants) before doing the change. This snapshots the affected lines into a new u_entry_T and links it to the current head. After the change, u_sync() may be called to seal the block and prepare for the next.

u_save_cursor();           // snapshot current line for undo
ml_replace(curwin->w_cursor.lnum, "new content", true);
changed_bytes(curwin->w_cursor.lnum, 0);

Forgetting u_save is one of the most common bugs in C code that mutates buffers. The symptom is that u doesn't undo your edit. The unit-test rule of thumb: if you're editing a buffer from C, u_save_* should be the first call you make.

The tree

graph TD
    R[root<br/>seq=0]
    R --> A[A<br/>seq=1]
    A --> B[B<br/>seq=2]
    B --> C[C<br/>seq=3]
    A --> D[D<br/>seq=4<br/>after undo to A]
    D --> E[E<br/>seq=5]

u follows prev pointers; <C-r> follows the most recently used next; g+ / g- walk by absolute time; :undo N jumps to sequence number N.

b_u_curhead is the "you are here" marker — undoing once moves it to prev, after which the buffer content matches that node's snapshot.

Persistent undo

When 'undofile' is on, the tree is serialized to <undodir>/<encoded-fname> on :write and read back on :edit. The format is documented at the top of undo.c#u_write_undo. It is versioned and includes a hash of the buffer content; if the file no longer matches, the saved tree is rejected.

The persistence format also stores extmark positions for the undo-restore feature: extmarks tagged with undo_restore=true are repositioned correctly on undo.

Integration points

  • Buffer mutationml_replace, ml_append, ml_delete all expect u_save_* to have been called first.
  • APInvim_buf_set_text, nvim_buf_set_lines call u_save on your behalf; you don't need to manually wrap them.
  • Extmarksundo_save_extmark() in marktree.c snapshots extmark positions per node so undo restores them.
  • Insert mode<C-g>u (do_break_undo) explicitly closes the current block, letting you undo back to the start of an Insert session in stages.
  • :earlier / :later — wall-clock-time traversal. ex_undolist shows the tree.

Entry points for modification

  • Add a new "save before change" path. New variant of u_save_* in change.c. Must be called before the mutation.
  • Persist a new payload alongside the tree. Extend the on-disk format in undo.c#u_write_undo and u_read_undo. Bump the version number; u_read_undo rejects mismatched versions.
  • Tweak the grouping heuristic. u_sync decides when a block ends. Be careful — most "feels weird" undo bugs trace back to this function.

Key source files

File Purpose
src/nvim/undo.c The whole undo machine — tree, traversal, persist
src/nvim/undo_defs.h u_header_T, u_entry_T, the tree connecting fields on buf_T
src/nvim/change.c u_save_cursor, u_save, u_savesub, u_savedel
src/nvim/ex_undo.c :undo, :redo, :undolist, :earlier, :later
src/nvim/api/buffer.c API mutators that handle u_save for the caller

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Undo – Neovim wiki | Factory