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 changeKey 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 mutation —
ml_replace,ml_append,ml_deleteall expectu_save_*to have been called first. - API —
nvim_buf_set_text,nvim_buf_set_linescallu_saveon your behalf; you don't need to manually wrap them. - Extmarks —
undo_save_extmark()inmarktree.csnapshots 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_undolistshows the tree.
Entry points for modification
- Add a new "save before change" path. New variant of
u_save_*inchange.c. Must be called before the mutation. - Persist a new payload alongside the tree. Extend the on-disk format in
undo.c#u_write_undoandu_read_undo. Bump the version number;u_read_undorejects mismatched versions. - Tweak the grouping heuristic.
u_syncdecides 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.