Hear the ghosts tell you where you were, and why you were there.
Bring back the past with haunt.nvim!
Annotate your codebase with ghost text. Search through the history that you choose.
Keep your mental overhead to a minimum and never repeatedly rummage through your codebase again.
- Virtual text annotations
- Keep your personal notes in your code without modifying the actual files
- Git integration
- annotations are tied to a git branch. Keep different notes for different branches
- Jump around using your hauntings
- Search through your bookmarks with
snacks.nvimortelescope.nvim - Use
sidekick.nvimto send your annotations to your favorite cli tool. Have a robot purge you of your hauntings! - Populate the quickfix list with your bookmarks when you want a simple jump list
- Though the examples don't show this, this plugin was deisgned to ease the navigation of massive codebases through semantic markings
- Super fast, and does its best to have as little computations, and load time, as possible. I get around .6ms ;)
- Neovim 0.11 - virtual text
- snacks.nvim - picker integration. (optional)
- telescope.nvim - picker integration. (optional)
- fzf-lua - picker integration. (optional)
- sidekick.nvim - 'AI' integration (and a cli tool of your choice). (optional)
return {
"TheNoeTrevino/haunt.nvim",
-- default config: change to your liking, or remove it to use defaults
---@class HauntConfig
opts = {
sign = "󱙝",
sign_hl = "DiagnosticInfo",
virt_text_hl = "HauntAnnotation", -- links to DiagnosticVirtualTextHint
annotation_prefix = " 󰆉 ",
line_hl = nil,
virt_text_pos = "eol",
data_dir = nil,
per_branch_bookmarks = true,
picker = "auto", -- "auto", "snacks", "telescope", or "fzf"
picker_keys = { -- picker agnostic, we got you covered
delete = { key = "d", mode = { "n" } },
edit_annotation = { key = "a", mode = { "n" } },
},
},
-- recommended keymaps, with a helpful prefix alias
init = function()
local haunt = require("haunt.api")
local haunt_picker = require("haunt.picker")
local map = vim.keymap.set
local prefix = "<leader>h"
-- annotations
map("n", prefix .. "a", function()
haunt.annotate()
end, { desc = "Annotate" })
map("n", prefix .. "t", function()
haunt.toggle_annotation()
end, { desc = "Toggle annotation" })
map("n", prefix .. "T", function()
haunt.toggle_all_lines()
end, { desc = "Toggle all annotations" })
map("n", prefix .. "d", function()
haunt.delete()
end, { desc = "Delete bookmark" })
map("n", prefix .. "C", function()
haunt.clear_all()
end, { desc = "Delete all bookmarks" })
-- move
map("n", prefix .. "p", function()
haunt.prev()
end, { desc = "Previous bookmark" })
map("n", prefix .. "n", function()
haunt.next()
end, { desc = "Next bookmark" })
-- picker
map("n", prefix .. "l", function()
haunt_picker.show()
end, { desc = "Show Picker" })
-- quickfix
map("n", prefix .. "q", function()
haunt.to_quickfix()
end, { desc = "Send Hauntings to QF Lix (buffer)" })
map("n", prefix .. "Q", function()
haunt.to_quickfix({ current_buffer = true })
end, { desc = "Send Hauntings to QF Lix (all)" })
-- yank
map("n", prefix .. "y", function()
haunt.yank_locations({current_buffer = true})
end, { desc = "Send Hauntings to Clipboard (buffer)" })
map("n", prefix .. "Y", function()
haunt.yank_locations()
end, { desc = "Send Hauntings to Clipboard (all)" })
end,
}2026-01-19-230449_hyprcap.mp4
By default, haunt.nvim provides no default keymaps. You will have to set them up yourself. See the installation section for an example. The installation section includes some recommended keymaps to get you started. You can also just use the user commands, which we will talk about later.
Here are the exposed API functions you should know about:
local haunt = require("haunt.api")
local haunt_picker = require("haunt.picker")
local haunt_sk = require("haunt.sidekick")
-- See `:h haunt-api` for more info on each function
-- Annotate the current line with a ghost text annotation,
-- or edit the annotation if it already exists
haunt.annotate()
-- Toggle visibility of the current annotation
haunt.toggle_annotation()
-- Toggle visibility of the all annotations
haunt.toggle_all_lines()
-- Remove all annotations in the workspace. Good for when you finish up a subtask
haunt.clear_all()
-- Delete annotation on the current line
haunt.delete()
-- Jump to the next/prev annotation in the buffer
haunt.next()
haunt.prev()
-- Open the bookmark picker.
--
-- Displays all bookmarks in an interactive picker.
-- Supports Snacks.nvim, Telescope.nvim, and fzf-lua (configurable via `picker` option).
-- Allows jumping to, deleting, or editing bookmark annotations.
-- see :h haunt-picker for more info
haunt_picker.show()
haunt_picker.show({ layout = { preset = "vscode" } }) -- Snacks options
haunt_picker.show({ prompt_title = "My Bookmarks" }) -- Telescope options
haunt_picker.show({ prompt = "Bookmarks> " }) -- fzf-lua options
-- Get bookmark locations formatted for sidekick.nvim.
-- Returns bookmarks in sidekick-compatible format:
-- `- @/{path} :L{line} - "{note}"`
-- see :h haunt-sidekick for more info, and the sidekick section below
haunt_sk.get_locations()
haunt_sk.get_locations({current_buffer = true})
--- Change the data directory and reload all bookmarks.
---
--- Saves current bookmarks to the old data_dir, clears all visual elements,
--- then loads bookmarks from the new location and restores visuals.
--- This is useful for autocommands that need to switch bookmark contexts.
haunt.change_data_dir("~/projects/myproject/.bookmarks/")
haunt.change_data_dir(nil) -- reset to defaultOr you can use the user commands:
HauntAnnotate
HauntClear
HauntClearAll
HauntDelete
HauntList
HauntNext
HauntPrev
HauntQf
HauntQfAll
HauntChangeDataDir [path]
It should be pretty obvious what these do.
If you wanna script something with this plugin, take a look at :h haunt.
I tried my best to expose as many useful functions as possible.
Search, edit, and delete annotations from the picker using snacks.nvim, telescope.nvim, or fzf-lua.
2026-01-19-231057_hyprcap.mp4
By default, haunt.nvim uses "auto" mode which tries Snacks first, then Telescope, then fzf-lua, then falls back to vim.ui.select.
You can explicitly choose your picker:
return {
"TheNoeTrevino/haunt.nvim",
opts = {
-- Choose your picker: "auto", "snacks", "telescope", or "fzf"
picker = "auto",
-- Customize picker keybindings (works for both Snacks and Telescope)
picker_keys = {
delete = {
key = "d",
mode = { "n" },
},
edit_annotation = {
key = "a",
mode = { "n" },
},
},
},
}Picker actions:
<CR>: Jump to the selected bookmarkd(normal mode): Delete the selected bookmarka(normal mode): Edit the bookmark's annotation
Send the position of your annotations to your favorite CLI tool through sidekick
2026-01-19-232635_hyprcap.mp4
Add this to your sidekick configuration:
local haunt_sk = require("haunt.sidekick")
return {
"folke/sidekick.nvim",
cmd = "Sidekick",
---@class sidekick.Config
opts = {
cli = {
prompts = {
haunt_all = function()
return haunt_sk.get_locations()
end,
haunt_buffer = function()
return haunt_sk.get_locations({ current_buffer = true })
end,
},
}
}
}Keep your annotations scoped to your branches.
2026-01-19-231639_hyprcap.mp4
Use change_data_dir to scope bookmarks per project/directory:
vim.api.nvim_create_autocmd("DirChanged", {
callback = function()
local project_bookmarks = vim.fn.getcwd() .. "/.bookmarks/"
require("haunt.api").change_data_dir(project_bookmarks)
end,
})I have tried all the bookmarking plugins out there, and none of them really fit my workflow. The specific issues I kept having were:
- Why is there a mark/bookmark here? Did I do that on purpose? Oh whatever...
- I wish I could fuzzy search the semantic meaning of the mark that I would have in my head.
- I want to send the marks, with my annotations, to my AI assistant to help me with my daily workflow.
- On massive codebases, I wish these marks had a 'why' to them.
The closest alternative I found was vim-bookmarks, but it is semi-broken, and the last commit was 5 years ago. Time for modern alternative!
I hope this helps others with the same issues.
- folke for snacks.nvim and sidekick.nvim. I use these plugins hundreds of times a day and they are amazing
- The API was extremely easy to work with. I also stole his CI/CD for my tests
- nvim-telescope team for telescope.nvim
- echasnovski for the mini.docs template. Having a way to automatically generate documentation from annotations is a lifesaver
Possible Improvement
- Users should be able to toggle notification. add a 'notify' option to config. this can take in a boolean, or a logging level. then, in the program, we can use a custom logger that uses vim.notify. we are gonna wanna use closure to store the log level and avoid recomputes
