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 callbacksKey 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
ccharif'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 (
undofiledoes not persist extmarks either).
The shada code lives in src/nvim/shada.c.
Integration points
- Buffer changes — every text mutation calls
extmark_splice()inextmark.c, which callsmarktree_splice(). Bypassing this leaves marks at stale positions. - Renderer —
decoration_redraw_*indecoration.care called fromdrawline.c#win_lineduring redraw. - Treesitter —
runtime/lua/vim/treesitter/highlighter.luaregisters 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
:signcommand andnvim_buf_set_extmarkwithsign_textboth go through the same marktree path now.
Entry points for modification
- A new decoration kind. Add it to
Decorationindecoration_defs.h, render it indrawline.c#win_line, expose it through thenvim_buf_set_extmarkoptstable. - A new mark behavior. Most "robust pointer" needs are met by extmarks; reach for
mark.conly 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.