Line-anchored notes for Neovim, under a single :Annotate command. A note is
displayed as virtual text at the end of its line and follows the line as the
file is edited.
| command | description |
|---|---|
Annotate [set] |
add or edit the note on the current line |
Annotate delete |
remove the note on the current line |
Annotate list |
select a note and jump to it |
Annotate qflist |
send every note to the quickfix list |
Annotate clear_file |
remove every note in the current file |
Annotate clear_all |
remove every note in the store |
Neovim >= 0.10. No other dependencies.
vim.pack, Neovim 0.12's built-in plugin manager:
vim.pack.add({ "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/mbfoss/annotate.nvim" }){ "mbfoss/annotate.nvim" }No setup call is required: :Annotate is registered when the plugin loads, and
the plugin's modules load on first use or when a file with notes is opened.
:Annotateasks for the note text on the current line. An existing note's text seeds the prompt for editing; an empty submission deletes it.- Notes show as virtual text (default), as a sign in the gutter, or both.
- Notes are extmarks, so they follow their line as you edit instead of sticking to a line number; the line number is saved when the buffer is saved.
- Deleting an annotated line does not delete the note: it moves to the line that takes its place.
:Annotate listpicks a note withvim.ui.selectand jumps to it, so avim.ui.selectoverride such as Telescope is worth having.:Annotate qflistsends them all to the quickfix list — better for reading through notes than for jumping to one.
Notes live in a single JSON file, stdpath("data")/annotate.json by default.
{
"notes": {
"/home/me/proj/lua/init.lua": [{ "lnum": 12, "text": "rewrite this" }]
}
}For notes per project, set storage_file to a function returning a path. It is
called when the notes are read and whenever the current directory changes, so
it can depend on that directory: change it and the notes of the project you
left are written out and replaced by the ones kept there.
require("annotate").setup({
storage_file = function()
local root = vim.fs.root(0, ".git")
if not root then return nil end -- no repository: use the default store
return vim.fs.joinpath(root, ".annotate.json")
end,
})That writes .annotate.json in the root of the current git repository — a file
to commit, or to add to .gitignore.
Behaviour:
- Paths are relative to the store's directory when they are under it, so a project store survives the project being moved or cloned; a note on a file outside it keeps its absolute path.
- The store is read when the plugin loads, and again when a directory change
points
storage_fileat a different file. - It is written whenever a note changes, when a buffer holding notes is saved, and on exit. A store with no notes left in it is deleted.
Several Neovim sessions can share a store:
- Each write merges into what is on disk: the notes this session added, edited or deleted win, and every other note is left alone.
- Notes are identified by file and line, so if two sessions annotate the same line, the one that writes last wins.
:Annotate clear_allis the exception — it empties the store, other sessions' notes included.- There is no locking. Two writes landing in the same instant can still lose one; sessions saving seconds or minutes apart — the normal case — will not.
setup() is optional and only needed to change a default.
require("annotate").setup({
symbol = "⚑", -- drawn before the note text
priority = 50, -- extmark priority of the virtual text
sign = "", -- one or two cells in the gutter; "" draws none
virt_text_pos = "eol", -- or "right_align", or "off" ("") for none
storage_file = nil, -- path, or a function returning one; defaults
-- to stdpath("data")/annotate.json
})| option | type | description |
|---|---|---|
symbol |
string | prefix drawn before the note text |
priority |
number | extmark priority for the virtual text |
sign |
string | sign placed in the gutter, one or two cells wide; "" draws none |
virt_text_pos |
string | extmark virt_text_pos: eol, right_align, or off (or "") for no virtual text |
storage_file |
string or function | JSON file the notes are written to; a function is called when the notes are read and on every current-directory change, and may return nil for the default store |
:checkhealth annotateReports:
- the command;
- the note store in force — what
storage_fileresolves to now, whether it exists yet, and whether its directory does; - the options that differ from the defaults;
- as a warning, any option name annotate does not define:
setup()merges the table wholesale, so a misspelled one would otherwise be accepted in silence.
| group | default | applies to |
|---|---|---|
AnnotateNote |
Todo |
the note's virtual text |
AnnotateSign |
AnnotateNote |
the note's sign in the gutter |
require("annotate.notes") exposes the functionality directly, for keymaps and
for use from other code.
local notes = require("annotate.notes")
notes.set(file, lnum, text) -- add or replace the note on a line
notes.get(file, lnum) -- its text, or nil
notes.remove(file, lnum) -- remove it, returns whether there was one
notes.list() -- every note: { file, lnum, text }, ordered
notes.clear_file(file)
notes.clear_all() -- empties the store, other sessions included
notes.set_at_cursor() -- the functions the commands call
notes.delete_at_cursor()
notes.select()
notes.qflist()
notes.clear_current_file()
notes.clear_all_confirm()vim.keymap.set("n", "<leader>na", require("annotate.notes").set_at_cursor)
vim.keymap.set("n", "<leader>nd", require("annotate.notes").delete_at_cursor)
vim.keymap.set("n", "<leader>nl", require("annotate.notes").select)MIT