A Neovim plugin that manages a LIFO stack of todos anchored to code positions to keep in mind the stack of task at hand.
Each todo remembers the file and line where it was created and displays a marker in the sign column whenever that file is open.
- Neovim ≥ 0.10
- folke/snacks.nvim (optional) — preferred picker UI
- nvim-telescope/telescope.nvim (optional) — used as picker when snacks is not available; falls back to
vim.ui.selectif neither is installed
{
"Mitra98t/TodoPile",
-- optional: add snacks or telescope for a richer picker UI
dependencies = { "folke/snacks.nvim" },
config = function()
require("todo_pile").setup()
end,
}Call setup() once, optionally passing a config table. All fields are optional.
require("todo_pile").setup({
-- Symbol displayed in the sign column next to a todo's line.
-- Any string up to 2 characters works (single emoji included).
sign_text = "●", -- default: "●"
-- Highlight applied to the sign column marker.
-- Accepts a highlight group name or a hex color string.
sign_hl = "DiagnosticHint", -- default: "DiagnosticHint"
-- sign_hl = "#ff8800", -- hex color alternative
-- When true, each todo's first letter is used as its sign column marker
-- instead of sign_text. Mutually exclusive with sign_text — a warning is
-- shown at startup if both are set.
sign_first_letter = false, -- default: false
-- When true, popping the top todo automatically jumps to the new top.
jump_after_pop = true, -- default: true
-- When true, the todo text is shown as ghost (virtual) text at the end of
-- its line.
ghost_text = false, -- default: false
-- Prefix displayed before the todo text when ghost_text is enabled.
-- true → the sign column marker glyph (sign_text or first letter) — default
-- false → no prefix, only the todo text
-- string → literal prefix, e.g. "TODO:" or "FIXME:"
ghost_text_prefix = true, -- default: true (marker glyph)
-- Highlight applied to the ghost text.
-- Accepts a highlight group name or a hex color string.
-- Defaults to the TodoPileGhostText group, which is linked to Comment.
ghost_text_hl = "Comment", -- default: links to "Comment"
-- ghost_text_hl = "#ff8800", -- hex color alternative
})| Command | Description |
|---|---|
:TodoPileAdd [text] |
Add a todo at the cursor position. If text is omitted a prompt is shown. |
:TodoPilePop |
Remove the most recent todo. Jumps to the new top if jump_after_pop is enabled. |
:TodoPileJump |
Jump to the file and line of the most recent todo without removing it. |
:TodoPileList |
Open a picker with todos in the current project. Select one to navigate to it. |
:TodoPileList! |
Same as above but shows todos from all projects. |
:TodoPileClose |
Open a picker with todos in the current project. Select one to delete it. |
:TodoPileClose! |
Same as above but shows todos from all projects. |
:TodoPileQuickfix |
Populate the quickfix list with todos in the current project and open it. |
:TodoPileReorder |
Open a floating window to manually reorder the stack. |
:TodoPileClearProject |
Delete all todos whose files belong to the current working directory (asks for confirmation). |
| Key | Action |
|---|---|
<C-k> / <C-Up> |
Move item up |
<C-j> / <C-Down> |
Move item down |
<CR> |
Save new order |
q / <Esc> |
Cancel without saving |
-- Suggested keymaps (adjust to your preference)
local map = vim.keymap.set
-- Add a todo at the current position (prompts for text)
map("n", "<leader>ta", "<cmd>TodoPileAdd<cr>", { desc = "Todo: add" })
-- Close (pop) the top todo and jump to the new top
map("n", "<leader>tx", "<cmd>TodoPilePop<cr>", { desc = "Todo: pop top" })
-- Jump to the top todo without removing it
map("n", "<leader>tj", "<cmd>TodoPileJump<cr>", { desc = "Todo: jump to top" })
-- Browse and navigate all todos
map("n", "<leader>tl", "<cmd>TodoPileList<cr>", { desc = "Todo: list" })
-- Browse and selectively close a todo
map("n", "<leader>tc", "<cmd>TodoPileClose<cr>", { desc = "Todo: close one" })
-- Reorder the stack
map("n", "<leader>tr", "<cmd>TodoPileReorder<cr>", { desc = "Todo: reorder" })
-- Browse todos from all projects
map("n", "<leader>tL", "<cmd>TodoPileList!<cr>", { desc = "Todo: list all projects" })
-- Send todos to the quickfix list
map("n", "<leader>tq", "<cmd>TodoPileQuickfix<cr>", { desc = "Todo: quickfix list" })
-- Clear all todos for the current project
map("n", "<leader>tX", "<cmd>TodoPileClearProject<cr>", { desc = "Todo: clear project" }){
"Mitra98t/TodoPile",
dependencies = { "folke/snacks.nvim" },
keys = {
{ "<leader>ta", "<cmd>TodoPileAdd<cr>", desc = "Todo: add" },
{ "<leader>tx", "<cmd>TodoPilePop<cr>", desc = "Todo: pop top" },
{ "<leader>tj", "<cmd>TodoPileJump<cr>", desc = "Todo: jump to top" },
{ "<leader>tl", "<cmd>TodoPileList<cr>", desc = "Todo: list" },
{ "<leader>tc", "<cmd>TodoPileClose<cr>", desc = "Todo: close one" },
{ "<leader>tr", "<cmd>TodoPileReorder<cr>", desc = "Todo: reorder" },
{ "<leader>tL", "<cmd>TodoPileList!<cr>", desc = "Todo: list all projects" },
{ "<leader>tq", "<cmd>TodoPileQuickfix<cr>", desc = "Todo: quickfix list" },
{ "<leader>tX", "<cmd>TodoPileClearProject<cr>", desc = "Todo: clear project" },
},
config = function()
require("todo_pile").setup({
sign_text = "●",
sign_hl = "DiagnosticHint",
jump_after_pop = true,
})
end,
}TodoPile exposes a few functions you can embed in any statusline plugin:
| Function | Returns |
|---|---|
require("todo_pile").count() |
Total number of todos across all projects |
require("todo_pile").project_count() |
Number of todos in the current working directory |
require("todo_pile").top_text() |
Text of the top todo in the current project, or "" |
lualine example:
-- in your lualine sections
{
function()
local n = require("todo_pile").project_count()
return n > 0 and ("● " .. n) or ""
end,
}Todos are persisted as JSON at:
$XDG_DATA_HOME/nvim/todo_pile.json
(vim.fn.stdpath("data") on your system — typically ~/.local/share/nvim/todo_pile.json.)
The file is rewritten on every change. Deleting it resets the pile.

