Open-Source Wikis

/

Neovim

/

Systems

/

Marks and extmarks

neovim/neovim

Marks and extmarks

Purpose

A mark is a robust pointer to a position in a buffer. Vim's classic marks ('a through 'z, 'A through 'Z, '0'9, the jumplist, the change list) are a small, fixed set with hand-coded shift-on-edit behavior. Neovim adds extmarks, a general-purpose mechanism that scales to thousands of marks per buffer and carries optional decoration data (highlight, virtual text, sign, conceal). The implementation backbone is the marktree — an in-tree B+-tree.

Directory layout

src/nvim/
├── mark.c (~62k bytes)         Vim-style named marks, jumplist, change list
├── mark.h, mark_defs.h
├── extmark.c                   Public extmark functions
├── extmark.h, extmark_defs.h
├── marktree.c (~75k bytes)     The B+-tree implementation
├── marktree.h, marktree_defs.h
├── decoration.c (~41k bytes)   Decorations attached to extmarks
├── decoration.h, decoration_defs.h
└── decoration_provider.c       Lazy decorations from plugin callbacks

Key abstractions

Type / function File Description
fmark_T mark_defs.h A classic Vim mark: file name + line/col + buffer pointer.
MarkTree marktree_defs.h The B+-tree root. Lives on buf_T->b_marktree.
MTKey marktree_defs.h A position in the tree: row/col plus mark id and flags.
MTPair marktree_defs.h A range mark — start/end key, used for highlight ranges and conceal regions.
Decoration decoration_defs.h The "payload" attached to an extmark id (hl group, virt_text, sign, ...).
marktree_put, marktree_get, marktree_del marktree.c Tree mutators.
marktree_splice marktree.c The "shift everything past row X by ΔY" operation called on every text change.

How it works — the marktree

The marktree is a B+-tree of MTKey entries sorted by (row, col). Each leaf node holds up to 9 keys; each internal node holds up to 9 child pointers and the row/col offsets where each child starts. This lets:

  • Lookup by (row, col) — O(log n) ⇒ a few cache lines per query.
  • Range query (all marks between two positions) — walk a contiguous slice of leaves.
  • Splice (shift all marks past row X by ΔY) — push the offset onto the affected interior nodes; lazily propagate down on next traversal.

The lazy splice is the key insight. A 1-line insertion in a buffer with 100,000 extmarks would be O(n) if you walked them all; the marktree records "everything past this point is shifted by 1" on the smallest set of internal nodes that covers the affected range, and the shift is only applied when an entry is actually queried. This is why nvim_buf_set_lines is fast on heavily-marked buffers.

The data layout is documented in the file header comment of src/nvim/marktree.c (which is one of the more careful comments in the tree — it walks through the invariants and gives an example).

Extmarks

extmark.c is the user-facing layer. The public API is:

  • nvim_buf_set_extmark(buf, ns, row, col, opts) → id — create or update.
  • nvim_buf_del_extmark(buf, ns, id) → bool — delete.
  • nvim_buf_get_extmark_by_id(buf, ns, id, opts) — look up.
  • nvim_buf_get_extmarks(buf, ns, start, end, opts) — range query.
  • nvim_create_namespace(name) → ns_id — get a namespace handle.

Extmarks are scoped by namespace — each plugin gets its own namespace and can clear all of its marks at once with nvim_buf_clear_namespace(buf, ns, ...). The namespace plus the user-facing extmark id are encoded into the MTKey's id field.

The optional opts carries a Decoration if the mark should affect rendering: highlight group, virtual text, sign text, conceal char, line-hl, hl-eol, virt_lines, ui-watched flag, undo-restore behavior, etc. Decorations are stored in a separate table keyed by extmark id, not inlined into MTKey, to keep the tree dense.

Decorations

decoration.c is the bridge between extmarks and the renderer. When win_line() (in drawline.c) renders a buffer line, it queries the marktree for everything intersecting that line and applies:

  • Highlights — paint a range of cells with a highlight group.
  • Virtual text — draw extra characters (inline, end-of-line, or above/below the line) that are not in the buffer.
  • Sign text — draw a glyph in the sign column.
  • Conceal — replace a range with a single cchar if 'conceallevel' is set.
  • Line highlight — paint the entire line.

decoration_provider.c is a callback path: a plugin registers on_buf / on_win / on_line / on_end callbacks via nvim_set_decoration_provider. The renderer calls these per-line during redraw, which lets the plugin compute highlights lazily for only the visible lines. Treesitter highlighting (runtime/lua/vim/treesitter/highlighter.lua) and the LSP semantic-tokens client are the canonical consumers.

Classic Vim marks

mark.c is the legacy path. It manages:

  • Named marks ('a ... 'z, 'A ... 'Z).
  • Numbered marks ('0 ... '9) for the jumplist.
  • The change list (g; / g,).
  • The jumplist (<C-o> / <C-i>).
  • Special marks ('<, '>, '', '., '^).

These are stored in fixed-size arrays on buf_T (per-buffer marks) and globally for 'A'Z' (cross-file marks, persisted in shada). The shift-on-edit logic is open-coded in mark.c#mark_adjust.

There has been ongoing work to migrate the legacy marks onto the marktree, but for now the two systems coexist.

Persistence (shada)

Some marks survive across nvim sessions:

  • Uppercase named marks → shada.
  • Jumplist → shada.
  • Change list → shada.
  • Extmarks → not by default; persistence is an opt-in plugin concern (undofile does not persist extmarks either).

The shada code lives in src/nvim/shada.c.

Integration points

  • Buffer changes — every text mutation calls extmark_splice() in extmark.c, which calls marktree_splice(). Bypassing this leaves marks at stale positions.
  • Rendererdecoration_redraw_* in decoration.c are called from drawline.c#win_line during redraw.
  • Treesitterruntime/lua/vim/treesitter/highlighter.lua registers a decoration provider that uses extmarks under the hood.
  • LSP — diagnostic display, inlay hints, semantic tokens, document highlight all use extmarks. See runtime/lua/vim/lsp/diagnostic.lua, inlay_hint.lua, semantic_tokens.lua.
  • Sign API — the legacy :sign command and nvim_buf_set_extmark with sign_text both go through the same marktree path now.

Entry points for modification

  • A new decoration kind. Add it to Decoration in decoration_defs.h, render it in drawline.c#win_line, expose it through the nvim_buf_set_extmark opts table.
  • A new mark behavior. Most "robust pointer" needs are met by extmarks; reach for mark.c only if the new pointer needs Vim semantics (e.g., a new flavor of jumplist).
  • Marktree tuning. The B+ branching factor and node size are constants at the top of marktree.c. The header comment documents the trade-offs if you really want to tune them.

Key source files

File Purpose
src/nvim/marktree.c The B+-tree implementation
src/nvim/extmark.c Public extmark API + splice on text change
src/nvim/decoration.c Decoration storage and per-line query
src/nvim/decoration_provider.c Callback-based decoration providers
src/nvim/mark.c Legacy named marks, jumplist, change list
src/nvim/api/extmark.c The nvim_buf_*_extmark and nvim_*_namespace API surface
src/nvim/highlight.c, highlight_group.c Highlight groups, namespaces, nvim_set_hl

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

Marks and extmarks – Neovim wiki | Factory