Modern undotree plugin for nvim
- Blazing Fast
- Mordern UI
- Live Syntax-Aware Diff: Instant, TreeSitter-powered diff previews with word-level inline highlighting.
- Auto-attaching Tree: The undo tree automatically follows you as you switch between buffers.
- Node Marks: Bookmark important undo states for quick navigation.
- Highly Customizable: Almost every aspect can be configured.
The built-in undotree is great — it ships with Neovim, requires zero setup, and covers the basics well. If that's all you need, stick with it. atone.nvim is for people who want a bit more. Here's a quick comparison:
| Feature | nvim 0.12 :undotree |
atone.nvim |
|---|---|---|
| Setup | None (built-in) | Plugin install |
| Tree visualization | Basic | Standard + compact |
| Diff preview | No | TreeSitter highlighted + word-level inline |
| Node marks | No | Persistent, numbered, named, fuzzy-findable |
| Custom labels | No | Yes |
| Auto-attach to buffers | No | Yes |
| Customization | Limited | Highly |
Both have their place, pick what fits your workflow.
You can install atone.nvim using your favorite plugin manager. Here comes a example for lazy.nvim
{
"XXiaoA/atone.nvim",
cmd = "Atone",
---@module "atone"
---@type AtoneConfig
opts = {},
}The main command is :Atone. It has the following subcommands:
| Command | Description |
|---|---|
:Atone or :Atone open |
Opens the undo tree view. |
:Atone toggle |
Toggles the undo tree view on and off. |
:Atone close |
Closes the undo tree view. |
:Atone focus |
Moves the cursor to the undo tree window. |
You can configure atone.nvim by passing a table to the setup function. Here are the default options:
require("atone").setup({
layout = {
---@type "left"|"right"
direction = "left",
---@type "adaptive"|number
--- adaptive: adapt to width of tree graph
--- float < 1: width = vim.o.columns * value
--- integer >= 1: absolute width
width = 0.25,
},
-- diff for the node under cursor
-- shown under the tree graph
diff_cur_node = {
enabled = true,
--- The diff window's height is set to a specified percentage of the original (namely tree graph) window's height.
split_percent = 0.3,
---@type "adaptive"|number
--- adaptive: same width as tree window (default)
--- float < 1: width = vim.o.columns * value
--- integer >= 1: absolute width
--- Note that non-adaptive values create a float diff window anchored to a hidden
--- dummy split window. this is an implementation detail that may cause
--- unexpected edge-case bugs in certain window layouts.
width = "adaptive",
-- Use TreeSitter to highlight the source code inside diff hunks.
treesitter = true,
-- Highlight the exact changed word ranges inside modified lines.
inline_diff = true,
},
-- automatically update the buffer that the tree is attached to
-- only works for buffer whose buftype is <empty>
auto_attach = {
enabled = true,
excluded_ft = { "oil" },
},
marks = {
persist = true,
persist_path = vim.fn.stdpath("data") .. "/atone_marks.json",
--- finders are tried in order. "builtin" is always available.
finders = { "fzf-lua", "telescope", "builtin" },
},
keymaps = {
tree = {
quit = { "<C-c>", "q" },
next_node = "j", -- support v:count
pre_node = "k", -- support v:count
jump_to_G = "G",
jump_to_gg = "gg",
undo_to = "<CR>",
set_mark = "m",
delete_mark = { "x", "X" },
delete_all_marks = "dM",
goto_mark = { "'", "`" },
mark_picker = "s",
help = { "?", "g?" },
},
auto_diff = {
quit = { "<C-c>", "q" },
help = { "?", "g?" },
undo = "u",
redo = "<C-r>",
},
help = {
quit_help = { "<C-c>", "q" },
},
},
ui = {
-- refer to `:h 'winborder'`
border = "single",
-- compact graph style
compact = false,
node_label = {
custom = false,
---@param ctx AtoneNodeLabelContext
---@return AtoneNodeLabel
formatter = function(ctx)
return string.format("[%d] %s %s", ctx.seq, ctx.h_time, ctx.bookmark or "")
end,
extmark_opts = { strict = false },
},
},
})The tree uses the built-in label format by default. If you want full control over the label, turn on ui.node_label.custom and provide a formatter(ctx).
require("atone").setup({
ui = {
node_label = {
custom = true,
---@param ctx AtoneNodeLabelContext
---@return AtoneNodeLabel
formatter = function(ctx)
return string.format("[%d] %s %s", ctx.seq, ctx.h_time, ctx.bookmark or "")
end,
},
},
})Available fields on ctx:
| Field | Type | Description |
|---|---|---|
seq |
integer |
Undo sequence number of the node |
is_current |
boolean |
Whether this node is the current undo state |
time |
integer |
Raw timestamp from Neovim's undotree |
h_time |
string |
Human-readable time string |
bookmark |
string? |
Bookmark label text, e.g. {a} |
diff |
{ added: integer, removed: integer } |
Added/removed line counts for the node |
Things to know:
- Fixed labels (
custom = false) are built for the whole tree when the tree is refreshed. - Custom labels only write label text for the visible part of the tree.
- When the viewport moves, the old visible range is restored to the plain graph text and the new visible range gets fresh label text.
- Highlight groups returned by chunked labels are applied only for the currently visible label range.
- Fixed-label highlighting is not used in custom mode. If you want highlighted custom labels, return highlighted chunks.
You may return either a plain string or a list of text chunks.
Plain string example:
formatter = function(ctx)
return string.format("[%d] %s %s", ctx.seq, ctx.h_time, ctx.bookmark or "")
endChunked example with per-segment highlight groups:
formatter = function(ctx)
return {
"[",
{ ctx.seq, ctx.is_current and "AtoneCurrentNode" or "AtoneSeq" },
"] ",
{ ctx.h_time, "Comment" },
ctx.bookmark and " " or "",
{ ctx.bookmark or "", "AtoneMark" },
}
endWhen you return chunks, chunks with a highlight group are highlighted and chunks without one are shown as normal text.
The keymaps table in the configuration allows you to map keys to specific actions in different windows. The keys can be a single string or a table of strings.
Here are the available actions and their default keybindings:
| Action | Default Key(s) | Description |
|---|---|---|
next_node |
j |
Jump to the next node in the undo tree. Supports v:count. |
pre_node |
k |
Jump to the previous node in the undo tree. Supports v:count. |
jump_to_G |
G |
Jump to the node with the specified sequence number like G |
jump_to_gg |
gg |
Jump to the node with the specified sequence number like gg |
undo_to |
<CR> |
Revert the buffer to the state of the node under the cursor. |
set_mark |
m |
Set a mark. Use N:name or N for slot (0-9). |
delete_mark |
x, X |
Delete the mark on the node under cursor. |
delete_all_marks |
dM |
Delete all marks in current buffer. |
goto_mark |
', ` |
Jump to a mark slot (0-9). |
mark_picker |
s |
Open mark picker (fuzzy find). |
undo |
u (diff window) |
Undo one step in the attached buffer. |
redo |
<C-r> (diff window) |
Redo one step in the attached buffer. |
quit |
<C-c>, q |
Close all atone.nvim windows (tree, diff, and help). |
help |
?, g? |
Show the help page. |
quit_help |
<C-c>, q |
Close the help window. |
atone.nvim uses the following highlight groups. You can customize them as what you did for normal highlight groups.
| Highlight Group | Default | Description |
|---|---|---|
AtoneSeq |
link to Number |
The sequence number of each node |
AtoneSeqBracket |
link to Comment |
The brackets surrounding the node sequence number |
AtoneCurrentNode |
link to Keyword |
The currently selected node in the undo tree |
AtoneMark |
link to BookmarkSign |
Mark labels on nodes |
AtoneDiffAdd |
link to DiffAdd |
Background for added lines in the diff preview |
AtoneDiffDelete |
link to DiffDelete |
Background for removed lines in the diff preview |
AtoneDiffAddInline |
derived from DiffAdd |
Adaptive inline background for the changed range |
AtoneDiffDeleteInline |
derived from DiffDelete |
Adaptive inline background for the changed range |
- Heavily inspired by vim-mundo
- Refer to user commands implementation in nvim-best-practices
- The inline diff and syntax highlighting architecture draws inspiration from diffs.nvim