Make annotations without leaving Neovim. Use annotator.nvim to leave
comments, rejection/approval marks, labels or suggest rewrites as you work.
When you are ready, hand off batches of annotations to LLMs or export
elsewhere. Annotations are exported as structured Markdown by default, but they
can be configured via templates.
annotator-demo-github.mp4
- Annotate lines and ranges with typed comments, suggestions, deletion marks, labels, optional persistence, and export hooks.
- No required external dependencies. If Snacks is installed,
:AnnotatorListuses its picker; otherwise it falls back to Neovim quickfix. - Keep annotations in memory by default, or opt into state-backed storage to restore unexported annotations on restart.
With vim.pack:
vim.pack.add({
{ src = "https://github.com/chpeters/annotator.nvim" },
})
require("annotator").setup()With lazy.nvim:
{
"chpeters/annotator.nvim",
opts = {},
}Annotate the current line:
:AnnotatorAddSuggest a replacement for a range:
:12,16AnnotatorSuggestMark a line for deletion:
:AnnotatorMarkDeleteAnnotate a visual selection with the default mapping:
<leader>aaThen export everything:
:AnnotatorExportBy default, export copies Markdown like this to the system clipboard:
Neovim annotations:
## lua/example.lua
- lines 12-16 (annotator-ann-1)
This branch should probably preserve the original error.
```lua
return Effect.fail(error)
```The default exporter clears annotations after copying them.
- Neovim 0.10 or newer.
- No required plugin dependencies. Snacks is optional and only improves
:AnnotatorList. - Git is optional. When available, annotator.nvim records repository root, branch, commit, and relative paths for exports.
- The default exporter uses Neovim clipboard support. On macOS it can also fall
back to
pbcopy. On Linux or Windows, configure a Neovim clipboard provider or replacehooks.export.
Default configuration:
require("annotator").setup({
mappings = true,
storage = "memory",
display = {
sign_text = "A>",
sign_hl_group = "AnnotatorAnnotationSign",
virtual_text_prefix = " Annotation: ",
virtual_text_hl_group = "AnnotatorAnnotationVirtual",
virtual_text_pos = "eol",
max_comment_length = 80,
priority = 120,
kinds = {
comment = { sign_text = "C>", virtual_text_prefix = " Comment: " },
suggest = { sign_text = "S>", virtual_text_prefix = " Suggest: " },
delete = { sign_text = "D>", virtual_text_prefix = " Delete: " },
label = { sign_text = "L>", virtual_text_prefix = " Label: " },
},
},
labels = {
{ id = "explain", title = "Explain", comment = "Please explain this more clearly." },
{ id = "clarify", title = "Clarify", comment = "Please clarify this point." },
{ id = "simplify", title = "Simplify", comment = "Please simplify this." },
{ id = "tighten", title = "Tighten", comment = "Please tighten this up." },
{ id = "expand", title = "Expand", comment = "Please expand on this." },
},
formatter = nil,
hooks = {
export = require("annotator").exporters.copy_to_clipboard,
},
})Options:
mappings: set tofalseto skip default mappings.storage:"memory"by default, or"state"to persist unexported annotations.storage_path: optional path override for"state"storage.display: sign and virtual text options.display.kinds: per-kind overrides forcomment,suggest,delete, andlabel.labels: label picker entries withid,title, and exportedcomment.formatter(ctx): optional function that returns exported Markdown.hooks.export(ctx): function called by:AnnotatorExport.
State storage defaults to:
stdpath("state")/annotator.nvim/annotations.json
State-backed storage writes only portable annotation fields. Neovim buffer IDs and extmark IDs are recreated for open buffers and are not persisted.
Example display tweak:
require("annotator").setup({
display = {
kinds = {
comment = { sign_text = "C>", virtual_text_prefix = " Comment: " },
suggest = { sign_text = "S>", virtual_text_prefix = " Suggest: " },
delete = { sign_text = "D>", virtual_text_prefix = " Delete: " },
label = { sign_text = "L>", virtual_text_prefix = " Label: " },
},
},
})By default, annotator.nvim renders generic Markdown grouped by file. Override
the rendered Markdown with formatter(ctx):
require("annotator").setup({
formatter = function(ctx)
-- ctx.annotations: pending typed annotations
-- ctx.default_format(ctx.annotations): built-in Markdown formatter
return ctx.default_format(ctx.annotations)
end,
})Provide hooks.export(ctx) to integrate with your own workflow:
require("annotator").setup({
hooks = {
export = function(ctx)
-- ctx.markdown: rendered Markdown
-- ctx.annotations: pending annotations
-- ctx.notify(message, kind): show a Neovim notification
-- ctx.clear_exported(): clear exported annotations after success
vim.fn.setreg("+", ctx.markdown)
ctx.clear_exported()
ctx.notify("Copied annotations", "info")
end,
},
})If the hook does not call ctx.clear_exported(), annotations remain pending.
:AnnotatorAdd: add an annotation for the current line or command range.:AnnotatorSuggest: edit a replacement for the current line or command range.:AnnotatorMarkDelete: mark the current line or command range for removal.:AnnotatorLabel: choose a configured label for the current line or command range.:AnnotatorEdit: edit the annotation at the cursor.:AnnotatorDelete: delete the annotation at the cursor.:AnnotatorList: browse pending annotations.:AnnotatorExport: run the configured export hook.:AnnotatorClear: clear pending annotations.
Default mappings:
- Normal
<leader>aa: add or edit the current-line annotation. - Visual
<leader>aa: annotate the selected lines. - Normal
<leader>ax: export annotations.
Typed commands do not claim additional default mappings; map them in your own config if you use them often.
local annotations = require("annotator")
annotations.setup(opts)
annotations.add() -- current line
annotations.add_visual()
annotations.suggest()
annotations.suggest_visual()
annotations.mark_delete()
annotations.mark_delete_visual()
annotations.label()
annotations.label_visual()
annotations.edit()
annotations.delete()
annotations.list()
annotations.render() -- Markdown for all pending annotations
annotations.export()
annotations.clear()- Adding on a line with an existing comment annotation edits that annotation instead of creating a duplicate comment.
- Adding a range edits an existing annotation of the same kind only when the file and range match exactly.
:AnnotatorSuggestopens a floating scratch buffer prefilled with the selected text. Normal-mode<CR>or<C-s>saves;qcancels.:AnnotatorLabelusesvim.ui.select, so your existing UI plugin can style it without annotator.nvim depending on that plugin.:AnnotatorEditis kind-aware: comments and deletion marks usevim.ui.input, suggestions reopen the replacement editor, and labels reopen the label picker.:AnnotatorListuses Snacks picker when available and falls back to quickfix.- The default exporter copies Markdown to the system clipboard.
MIT. See LICENSE.