README.md

July 30, 2026 · View on GitHub

atone.nvim

Modern undotree plugin for nvim

Stars Issues License

Image

Features

  • Blazing Fast
  • Modern 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.

How it relates to nvim's built-in :undotree?

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:

Featurenvim 0.12 :undotreeatone.nvim
SetupNone (built-in)Plugin install
Tree visualizationBasicStandard + compact
Diff previewNoTreeSitter highlighted + word-level inline
Node marksNoPersistent, numbered, named, fuzzy-findable
Custom labelsNoYes
Auto-attach to buffersNoYes
CustomizationLimitedHighly

Both have their place, pick what fits your workflow.

Installation

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 = {},
}

Commands

The main command is :Atone. It has the following subcommands:

CommandDescription
:Atone or :Atone openOpens the undo tree view.
:Atone toggleToggles the undo tree view on and off.
:Atone closeCloses the undo tree view.
:Atone focusMoves the cursor to the undo tree window.

Configuration

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?" },
            float_diff = "gd",
        },
        auto_diff = {
            quit = { "<C-c>", "q" },
            help = { "?", "g?" },
            undo = "u",
            redo = "<C-r>",
            float_diff = "gd",
        },
        help = {
            quit_help = { "<C-c>", "q" },
        },
    },
    diff_float = {
        --- width of the diff float as a fraction of the editor width (0–1)
        width = 0.8,
        --- height of the diff float as a fraction of the editor height (0–1)
        height = 0.8,
        --- close the centred diff float when it loses focus.
        autoclose = true,
    },
    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%s",
                    ctx.seq,
                    ctx.h_time,
                    ctx.bookmark or "",
                    ctx.is_sticky_ref and " [=]" or ""
                )
            end,
            extmark_opts = { strict = false },
        },
    },
})

Custom Labels

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)
                local sticky = ctx.is_sticky_ref and " [=]" or ""
                return string.format("[%d] %s %s%s", ctx.seq, ctx.h_time, ctx.bookmark or "", sticky)
            end,
        },
    },
})

Available fields on ctx:

FieldTypeDescription
seqintegerUndo sequence number of the node
is_currentbooleanWhether this node is the current undo state
is_sticky_refbooleanWhether this node is the pinned sticky diff base
timeintegerRaw timestamp from Neovim's undotree
h_timestringHuman-readable time string
bookmarkstring?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)
    local sticky = ctx.is_sticky_ref and " [=]" or ""
    return string.format("[%d] %s %s%s", ctx.seq, ctx.h_time, ctx.bookmark or "", sticky)
end

Chunked 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" },
        ctx.is_sticky_ref and " " or "",
        ctx.is_sticky_ref and { "[=]", "AtoneStickyRef" } or "",
    }
end

When you return chunks, chunks with a highlight group are highlighted and chunks without one are shown as normal text.

Keymap Actions

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:

ActionDefault Key(s)Description
next_nodejJump to the next node in the undo tree. Supports v:count.
pre_nodekJump to the previous node in the undo tree. Supports v:count.
jump_to_GGJump to the node with the specified sequence number like G
jump_to_ggggJump 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_markmSet a mark. Use N:name or N for slot (0-9).
delete_markx, XDelete the mark on the node under cursor.
delete_all_marksdMDelete all marks in current buffer.
goto_mark', `Jump to a mark slot (0-9).
mark_pickersOpen mark picker (fuzzy find).
set_sticky_ref=Toggle a sticky diff base.
undouUndo one step in the attached buffer.
redo<C-r>Redo one step in the attached buffer.
float_diffgdToggle a centred floating diff window.
quit<C-c>, qClose all atone.nvim windows (tree, diff, and help).
help?, g?Show the help page.
quit_help<C-c>, qClose the help window.

Highlighting

atone.nvim uses the following highlight groups. You can customize them as what you did for normal highlight groups.

Highlight GroupDefaultDescription
AtoneSeqlink to NumberThe sequence number of each node
AtoneSeqBracketlink to CommentThe brackets surrounding the node sequence number
AtoneCurrentNodelink to KeywordThe currently selected node in the undo tree
AtoneStickyReflink to DebugThe pinned sticky diff reference node
AtoneMarklink to BookmarkSignMark labels on nodes
AtoneDiffAddlink to DiffAddBackground for added lines in the diff preview
AtoneDiffDeletelink to DiffDeleteBackground for removed lines in the diff preview
AtoneDiffAddInlinederived from DiffAddAdaptive inline background for the changed range
AtoneDiffDeleteInlinederived from DiffDeleteAdaptive inline background for the changed range

Credits