Ghost text bookmarks for Neovim
Find a file
2026-06-06 22:07:41 -05:00
.github ci: add nighly to docs generation 2026-05-01 00:44:30 -05:00
doc chore(build): auto-generated vimdocs 2026-05-29 05:54:20 +00:00
lua/haunt Merge pull request #91 from marcomayer/feature/above-annotations 2026-05-29 00:53:08 -05:00
plugin feat(plugin): register :HauntReload command 2026-05-10 16:19:13 -05:00
scripts docs: add migration docs and update in general 2026-04-30 15:36:11 -05:00
tests Merge pull request #91 from marcomayer/feature/above-annotations 2026-05-29 00:53:08 -05:00
.gitignore ignore: hide test output 2026-01-19 06:04:36 +00:00
CHANGELOG.md chore(main): release 1.3.0 2026-06-07 01:56:31 +00:00
CONTRIBUTING.md feat(watcher): auto-reload bookmarks on branch checkout 2026-05-10 16:19:13 -05:00
LICENSE docs: plugin docs 2026-01-19 05:48:12 +00:00
README.md feat: above borders config options, demo to readme 2026-05-29 00:53:08 -05:00
stylua.toml add: stylua config 2026-01-19 06:04:36 +00:00

haunt.nvim 👻

IMG_0236(1)

Showcase


General Usage

Picker

Sidekick Integration

Git Branch Scope

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.

Features

  • 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.nvim or telescope.nvim
  • Use sidekick.nvim to 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 ;)

Requirements

  • 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)

Installation

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 = " 󰆉 ",
    annotation_suffix = "",
    line_hl = nil,
    virt_text_pos = "eol",
    above_max_width = 80,
    above_border = "rounded", -- "single", "double", "none", or character array
    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,
}

Above Annotations

Click to expand

Set virt_text_pos = "above" to display annotations in a bordered box above the line instead of inline. The box wraps long text and adapts to the window width.

  • above_max_width — maximum box width in columns (default 80, clamped to window width)
  • above_border — border style: "rounded" (default), "single", "double", "none", or a character array like {"╭", "─", "╮", "│", "╯", "─", "╰", "│"}

Above annotation demo

Usage

API

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 default

User Commands

Or you can use the user commands:

HauntAnnotate

HauntClear

HauntClearAll

HauntDelete

HauntList

HauntNext

HauntPrev

HauntQf

HauntQfAll

HauntChangeDataDir [path]

HauntMigrate

HauntReload

Take a look at :h haunt-commands for more details on each command, and examples of how to use them.

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.

Integrations

Picker (snacks.nvim / telescope.nvim / fzf-lua)

Click to expand

Search, edit, and delete annotations from the picker using snacks.nvim, telescope.nvim, or fzf-lua.

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 bookmark
  • d (normal mode): Delete the selected bookmark
  • a (normal mode): Edit the bookmark's annotation

sidekick.nvim

Click to expand

Send the position of your annotations to your favorite CLI tool through sidekick

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,
      },
    }
  }
}

Persistence, Sharing, and Migration

Bookmarks are saved as one JSON file per project, per branch, keyed by your git root commit, with project-relative paths inside. This makes the file portable across machines, forks, and checkouts.

Here are some ideas for how to use this for inspiration:

  • Store haunt annotation on a NAS and load them across machines (you can use tailscale!!)
  • Share with teammates by committing the bookmark files to git, and pointing everyone to the same data_dir
    • data can be vim.fn.getcwd() .. "/.haunt/"

For more details, see the help docs:

  • :h haunt-persistence — storage format and project keying
  • :h haunt-sharing — share with teammates via git, or sync privately across your own machines (NAS, Tailscale, private repo)
  • :h haunt-migration — upgrading from the v1 format

The goal of this was to be flexible enough to support a variety of workflows, while still being simple and intuitive to use out of the box.

Have fun!

Project-Specific Bookmarks

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,
})

Why?

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.

Acknowledgements

  • folke for snacks.nvim and sidekick.nvim, the API was extremely easy to work with
  • nvim-telescope team for telescope.nvim
  • mini.nvim for the mini.docs template

Similar Plugins