Skip to content

bngarren/checkmate.nvim

Repository files navigation

checkmate_logo

Get stuff done

Lua Neovim GitHub Release GitHub Actions Workflow Status


A Markdown-based todo/task plugin for Neovim.

Features

  • Saves files in plain Markdown format (compatible with other apps)
  • Customizable markers and styling
  • Visual mode support for toggling multiple items at once
  • Metadata e.g. @tag(value) annotations with extensive customization
    • e.g. @started, @done, @priority, @your-custom-tag
  • Todo completion counts/percentage
  • Smart toggling behavior
  • Archive completed todos
  • Todo templates with LuaSnip snippet integration
  • Custom todo states
    • More than just "checked" and "unchecked", e.g. "partial", "in-progress", "on-hold"
  • 🆕 Automatic todo creation (list continuation in insert mode)

Note

Check out the Wiki for additional documentation and recipes, including:


checkmate_example_simple checkmate_demo_complex
Checkmate.v0.11.mp4

Table of Contents


☑️ Installation

Requirements

  • Neovim 0.10 or higher

Using lazy.nvim

{
  "bngarren/checkmate.nvim",
  ft = "markdown", -- Lazy loads for Markdown files matching patterns in 'files'
  opts = {
    -- files = { "*.md" }, -- any .md file (instead of defaults)
  },
}

If you'd like stable-ish version during pre-release, can add a minor version to the lazy spec:

{
  version = "~0.11.0" -- pins to minor 0.11.x
}

☑️ Usage

1. Open or Create a Todo File

Checkmate automatically activates when you open a Markdown file that matches your configured file name patterns.

Default patterns:

  • todo or TODO (exact filename)
  • todo.md or TODO.md
  • Files with .todo extension (e.g., project.todo, work.todo.md)

Note

Checkmate only activates for files with the "markdown" filetype. Files without extensions need their filetype set to markdown (:set filetype=markdown)


You can customize which files activate Checkmate using the files configuration option:

files = {
  "*.md",              -- Any markdown file (basename matching)
  "**/todo.md",        -- 'todo.md' anywhere in directory tree
  "project/todo.md",   -- Any path ending with 'project/todo.md'
  "/absolute/path.md", -- Exact absolute path match
}

Patterns support Unix-style globs including *, **, ?, [abc], and {foo,bar}

2. Create Todos

  • Use the mapped key (default: <leader>Tn) or the :Checkmate create command
  • Or manually using Markdown syntax:
- [ ] Unchecked todo
- [x] Checked todo

(These will automatically convert when you leave insert mode!)

3. Manage Your Tasks

  • Toggle items with :Checkmate toggle (default: <leader>Tt)
  • Check items with :Checkmate check (default: <leader>Tc)
  • Uncheck items with :Checkmate uncheck (default: <leader>Tu)
  • Cycle to other custom states with :Checkmate cycle_next (default: <leader>T=) and :Checkmate cycle_previous (default <leader>T-)
  • Select multiple items in visual mode and use the same commands
  • Archive completed todos with :Checkmate archive (default: <leader>Ta)

Enhance your todos with custom metadata with quick keymaps!

The Checkmate buffer is saved as regular Markdown which means it's compatible with any Markdown editor!

☑️ Commands

User commands

:Checkmate [subcommand]

subcommand Description
archive Archive all completed todo items in the buffer. This extracts them and moves them to a bottom section. See api archive() and Archiving section.
check Mark the todo item under the cursor as checked. See api check()
create In normal mode, converts the current line into a todo (or if already a todo, creates a sibling below). In visual mode, converts each selected line into a todo. In insert mode, creates a new todo on the next line and keeps you in insert mode. For more advanced placement, indentation, and state options, see the create(opts) API.
cycle_next Cycle a todo's state to the next available. See api cycle()
cycle_previous Cycle a todo's state to the previous. See api cycle()
lint Lint this buffer for Checkmate formatting issues. Runs automatically on InsertLeave and TextChanged. See api lint() andLinting section.
metadata add Add a metadata tag to the todo under the cursor or within the selection. Usage: :Checkmate metadata add <key> [value]. See api add_metadata(key, value) and Metadata section.
metadata jump_next Move the cursor to the next metadata tag for the todo item under the cursor. See api jump_next_metadata()
metadata jump_previous Move the cursor to the previous metadata tag for the todo item under the cursor. See api jump_previous_metadata()
metadata remove Remove a specific metadata tag from the todo under the cursor or within the selection. Usage: :Checkmate metadata remove <key>. See api remove_metadata(key)
metadata select_value Select a value from the 'choices' option for the metadata tag under the cursor. See api select_metadata_value()
metadata toggle Toggle a metadata tag on/off for the todo under the cursor or within the selection. Usage: :Checkmate metadata toggle <key> [value]. See api toggle_metadata(key, value)
remove Convert a todo line back to regular text. See api remove(opts). By default, will preserve the list item marker and remove any metadata. This can be configured via opts.
remove_all_metadata Remove all metadata tags from the todo under the cursor or within the selection. See api remove_all_metadata()
toggle Toggle the todo item under the cursor (normal mode) or all todo items within the selection (visual mode). See api toggle(). Without a parameter, toggles between unchecked and checked. To change to custom states, use the api toggle(target_state) or the cycle_* commands.
uncheck Mark the todo item under the cursor as unchecked. See api uncheck()

☑️ Config

For config definitions/annotations, see here.

Defaults

---@type checkmate.Config
return {
  enabled = true,
  notify = true,
  -- Default file matching:
  --  - Any `todo` or `TODO` file, including with `.md` extension
  --  - Any `.todo` extension (can be ".todo" or ".todo.md")
  -- To activate Checkmate, the filename must match AND the filetype must be "markdown"
  files = {
    "todo",
    "TODO",
    "todo.md",
    "TODO.md",
    "*.todo",
    "*.todo.md",
  },
  log = {
    level = "warn",
    use_file = true,
  },
  -- Default keymappings
  keys = {
    ["<leader>Tt"] = {
      rhs = "<cmd>Checkmate toggle<CR>",
      desc = "Toggle todo item",
      modes = { "n", "v" },
    },
    ["<leader>Tc"] = {
      rhs = "<cmd>Checkmate check<CR>",
      desc = "Set todo item as checked (done)",
      modes = { "n", "v" },
    },
    ["<leader>Tu"] = {
      rhs = "<cmd>Checkmate uncheck<CR>",
      desc = "Set todo item as unchecked (not done)",
      modes = { "n", "v" },
    },
    ["<leader>T="] = {
      rhs = "<cmd>Checkmate cycle_next<CR>",
      desc = "Cycle todo item(s) to the next state",
      modes = { "n", "v" },
    },
    ["<leader>T-"] = {
      rhs = "<cmd>Checkmate cycle_previous<CR>",
      desc = "Cycle todo item(s) to the previous state",
      modes = { "n", "v" },
    },
    ["<leader>Tn"] = {
      rhs = "<cmd>Checkmate create<CR>",
      desc = "Create todo item",
      modes = { "n", "v" },
    },
    ["<leader>Tr"] = {
      rhs = "<cmd>Checkmate remove<CR>",
      desc = "Remove todo marker (convert to text)",
      modes = { "n", "v" },
    },
    ["<leader>TR"] = {
      rhs = "<cmd>Checkmate remove_all_metadata<CR>",
      desc = "Remove all metadata from a todo item",
      modes = { "n", "v" },
    },
    ["<leader>Ta"] = {
      rhs = "<cmd>Checkmate archive<CR>",
      desc = "Archive checked/completed todo items (move to bottom section)",
      modes = { "n" },
    },
    ["<leader>Tv"] = {
      rhs = "<cmd>Checkmate metadata select_value<CR>",
      desc = "Update the value of a metadata tag under the cursor",
      modes = { "n" },
    },
    ["<leader>T]"] = {
      rhs = "<cmd>Checkmate metadata jump_next<CR>",
      desc = "Move cursor to next metadata tag",
      modes = { "n" },
    },
    ["<leader>T["] = {
      rhs = "<cmd>Checkmate metadata jump_previous<CR>",
      desc = "Move cursor to previous metadata tag",
      modes = { "n" },
    },
  },
  default_list_marker = "-",
  todo_states = {
    -- we don't need to set the `markdown` field for `unchecked` and `checked` as these can't be overriden
    ---@diagnostic disable-next-line: missing-fields
    unchecked = {
      marker = "",
      order = 1,
    },
    ---@diagnostic disable-next-line: missing-fields
    checked = {
      marker = "",
      order = 2,
    },
  },
  style = {}, -- override defaults
  enter_insert_after_new = true, -- Should enter INSERT mode after `:Checkmate create` (new todo)
  list_continuation = {
    enabled = true,
    split_line = true,
    keys = {
      ["<CR>"] = function()
        require("checkmate").create({
          position = "below",
          indent = false,
        })
      end,
      ["<S-CR>"] = function()
        require("checkmate").create({
          position = "below",
          indent = true,
        })
      end,
    },
  },
  smart_toggle = {
    enabled = true,
    include_cycle = false,
    check_down = "direct_children",
    uncheck_down = "none",
    check_up = "direct_children",
    uncheck_up = "direct_children",
  },
  show_todo_count = true,
  todo_count_position = "eol",
  todo_count_recursive = true,
  use_metadata_keymaps = true,
  metadata = {
    -- Example: A @priority tag that has dynamic color based on the priority value
    priority = {
      style = function(context)
        local value = context.value:lower()
        if value == "high" then
          return { fg = "#ff5555", bold = true }
        elseif value == "medium" then
          return { fg = "#ffb86c" }
        elseif value == "low" then
          return { fg = "#8be9fd" }
        else -- fallback
          return { fg = "#8be9fd" }
        end
      end,
      get_value = function()
        return "medium" -- Default priority
      end,
      choices = function()
        return { "low", "medium", "high" }
      end,
      key = "<leader>Tp",
      sort_order = 10,
      jump_to_on_insert = "value",
      select_on_insert = true,
    },
    -- Example: A @started tag that uses a default date/time string when added
    started = {
      aliases = { "init" },
      style = { fg = "#9fd6d5" },
      get_value = function()
        return tostring(os.date("%m/%d/%y %H:%M"))
      end,
      key = "<leader>Ts",
      sort_order = 20,
    },
    -- Example: A @done tag that also sets the todo item state when it is added and removed
    done = {
      aliases = { "completed", "finished" },
      style = { fg = "#96de7a" },
      get_value = function()
        return tostring(os.date("%m/%d/%y %H:%M"))
      end,
      key = "<leader>Td",
      on_add = function(todo_item)
        require("checkmate").set_todo_item(todo_item, "checked")
      end,
      on_remove = function(todo_item)
        require("checkmate").set_todo_item(todo_item, "unchecked")
      end,
      sort_order = 30,
    },
  },
  archive = {
    heading = {
      title = "Archive",
      level = 2, -- e.g. ##
    },
    parent_spacing = 0, -- no extra lines between archived todos
    newest_first = true,
  },
  linter = {
    enabled = true,
  },
}

Keymapping

Default keymaps can be disabled by setting keys = false.

Keymaps should be defined as a dict-like table or a sequence of {rhs, desc?, modes?}.

keys = {
  ["<leader>Ta"] = {
      rhs = "<cmd>Checkmate archive<CR>",
      desc = "Archive todos",
      modes = { "n" },
  },
}

or

keys = {
  ["<leader>Ta"] = {"<cmd>Checkmate archive<CR>", "Archive todos", {"n"} }
}

The rhs parameter follows :h vim.keymap.set() and can be a string or Lua function.

Styling

Default styles are calculated based on the current colorscheme. This attempts to provide reasonable out-of-the-box defaults based on colorscheme-defined hl groups and contrast ratios.

Individual styles can still be overriden using the style option and passing a 'highlight definition map' according to :h nvim_set_hl() and vim.api.keyset.highlight for the desired highlight group (see below).

Highlight groups

hl_group description
CheckmateListMarkerUnordered Unordered list markers, e.g. -,*, and +. (Only those associated with a todo)
CheckmateListMarkerOrdered Ordered list markers, e.g. 1., 2). (Only those associated with a todo)
CheckmateUncheckedMarker Unchecked todo marker, e.g. . See todo_states marker option
CheckmateUncheckedMainContent The main content of an unchecked todo (typically the first paragraph)
CheckmateUncheckedAdditionalContent Additional content for an unchecked todo (subsequent paragraphs, list items, etc.)
CheckmateCheckedMarker Checked todo marker, e.g. . See todo_states marker option
CheckmateCheckedMainContent The main content of a checked todo (typically the first paragraph)
CheckmateCheckedAdditionalContent Additional content for a checked todo (subsequent paragraphs, list items, etc.)
CheckmateTodoCountIndicator The todo count indicator, e.g. 1/4, shown on the todo line, if enabled. See show_todo_count option

Metadata highlights are prefixed with CheckmateMeta_ and keyed with the tag name and style.

Main content versus Additional content

Highlight groups with 'MainContent' refer to the todo item's first paragraph. 'AdditionalContent' refers to subsequent paragraphs, list items, etc.

checkmate_main_vs_additional_hl_groups

Example: Change the checked marker to a bold green

opts = {
    style = {
        CheckmateCheckedMarker = { fg = "#7bff4f", bold = true}
    }
}

Example: Style a custom todo state

Custom todo states will be styled following the same highlight group naming convention: e.g. Checkmate[State]Marker So, if you define a partial state:

todo_states = {
  partial = {
    -- ...
  }
}

You can then style it:

styles = {
  CheckmatePartialMarker = { fg = "#f0fc03" }
  CheckmatePartialMainContent = { fg = "#faffa1" }
}

State names will be converted to CamelCase when used in highlight group names. E.g. not_planned = NotPlanned

Todo states

Checkmate supports both standard GitHub-flavored Markdown states and custom states for more nuanced task management.

Default states

The standard states are checked and unchecked, which are always saved to disk as - [ ] and - [x] per the Github-flavored Markdown spec. You can customize their visual appearance with the todo_states marker opt:

todo_states = {
  checked = {
    marker = "" -- how it appears in Neovim
  },
  unchecked = {
    marker = "" -- how it appears in Neovim
  }
}

Custom states

Add custom states to track tasks more precisely. Each state needs:

  • marker: How it appears in Neovim (must be unique)
  • markdown: How it's saved to disk (must be unique)

    and optionally:
  • type: How it behaves in the task hierarchy
todo_states = {
  -- Built-in states (cannot change markdown or type)
  unchecked = { marker = "" },
  checked = { marker = "" },
  
  -- Custom states
  in_progress = {
    marker = "",
    markdown = ".",     -- Saved as `- [.]`
    type = "incomplete", -- Counts as "not done"
    order = 50,
  },
  cancelled = {
    marker = "",
    markdown = "c",     -- Saved as `- [c]` 
    type = "complete",   -- Counts as "done"
    order = 2,
  },
  on_hold = {
    marker = "",
    markdown = "/",     -- Saved as `- [/]`
    type = "inactive",   -- Ignored in counts
    order = 100,
  }
}
checkmate_custom_states

State types

States have three behavior types that affect smart toggle and todo counts:

Type Behavior Example States
incomplete Counts as "not done" unchecked, in_progress, pending, future
complete Counts as "done" checked, cancelled
inactive Ignored in calculations on_hold, not_planned

Warning

Custom states like - [.] or - [/] are not standard Markdown and may not be recognized by other apps.

You can then cycle through a todo's states with :Checkmate cycle_next and :Checkmate cycle_previous or using the API, such as:

require("checkmate").cycle()        -- Next state
require("checkmate").cycle(true)    -- Previous state

-- or to toggle to a specific state
require("checkmate").toggle("on_hold")

Todo count indicator

Shows completion progress for todos with subtasks.

It displays the number of complete / incomplete todos in a hierarchy. It counts the standard "checked" and "unchecked" states, as well as custom states based on their type (incomplete or complete). The "inactive" type is not included.

checkmate_todo_indicator_eol
Todo count indicator using eol position
checkmate_todo_indicator_inline
Todo count indicator using inline position

Change the default display by passing a custom formatter

Basic example

-- Custom formatter that returns the % completed
todo_count_formatter = function(completed, total)
  return string.format("[%.0f%%]", completed / total * 100)
end,
style = {
  CheckmateTodoCountIndicator = { fg = "#faef89" },
},
checkmate_todo_count_percentage
Todo count indicator using todo_count_formatter function

Progress bar example, see Wiki for code.

checkmate_progress_bar_example

Count all nested todo items

If you want the todo count of a parent todo item to include all nested todo items, set the todo_count_recursive option.

checkmate_todo_indicator_recursive_false
todo_count_recursive false. Only direct children are counted.
checkmate_todo_indicator_recursive_true
todo_count_recursive true. All children are counted.

Smart Toggle

Intelligently propagates a todo's state change through its hierarchy.

When you toggle a todo item, it can automatically update related todos based on your configuration.

Note

Smart toggle only propagates "unchecked" and "checked" states (the default/standard todo states). If custom todo states are used, they may influence parent completion but will not be changed themselves.

How it works

Smart toggle operates in two phases:

  1. Downward propagation: When toggling a parent, optionally propagate the state change to children
  2. Upward propagation: When toggling a child, optionally update the parent based on children states

Configuration

Smart toggle is enabled by default with sensible defaults. You can customize the behavior:

opts = {
  smart_toggle = {
    enabled = true,
    check_down = "direct",        -- How checking a parent affects children
    uncheck_down = "none",        -- How unchecking a parent affects children
    check_up = "direct_children", -- When to auto-check parents
    uncheck_up = "direct_children", -- When to auto-uncheck parents
  }
}

☑️ Metadata

Metadata tags allow you to add custom @tag(value) annotations to todo items.

checkmate_metadata_example
  • Default tags:
    • @started - default value is the current date/time
    • @done - default value is the current date/time
    • @priority - "low" | "medium" (default) | "high"

The default tags are not deeply merged in order to avoid unexpected behavior. If you wish to modify a default metadata, you should copy the default implementation.

By configuring a metadata's choices option, you can populate your own lists of metadata values for powerful workflows, e.g. project file names, Git branches, PR's, issues, etc., team member names, external APIs, etc.

For in-depth guide and recipes for custom metadata, see the Wiki page.

☑️ Archiving

Allows you to easily reorganize the buffer by moving all completed todo items to a Markdown section beneath all other content. The remaining unchecked/incomplete todos are reorganized up top and spacing is adjusted.

Archiving collects all todos with the "completed" state type, which includes the default "checked" state, but possibly others based on custom todo states.

See Checkmate archive command or require("checkmate").archive()

Current behavior (could be adjusted in the future): a completed todo item that is nested under an incomplete parent will not be archived. This prevents 'orphan' todos being separated from their parents. Similarly, a completed parent todo will carry all nested todos (completed and incomplete) when archived.

Heading

By default, a Markdown level 2 header (##) section named "Archive" is used. You can configure the archive section heading via config.archive.heading

The following will produce an archive section labeled:

#### Completed
opts = {
  archive = {
    heading = {
      title = "Completed",
      level = 4
    }
  }
}

Spacing

The amount of blank lines between each archived todo item can be customized via config.archive.parent_spacing

E.g. parent_spacing = 0

## Archive

-Update the dependencies 
-Refactor the User api
-Add additional tests 

E.g. parent_spacing = 1

## Archive

-Update the dependencies 

-Refactor the User api

-Add additional tests 

☑️ Integrations

Please see Wiki for additional details/recipes.

Integration Capable?
render-markdown wiki
LuaSnip wiki
scratch buffer/floating window for quick todos, e.g. snacks.nvim wiki

☑️ Linting

Checkmate uses a very limited custom linter in order require zero dependencies but attempt to warn the user of Markdown (CommonMark spec) formatting issues that could cause unexpected plugin behavior.

The embedded linter is NOT a general-purpose Markdown linter and may interfere with other linting tools. Though, in testing with conform.nvim and prettier, I have not found any issues.

Example

❌ misaligned list marker

1. ☐ Parent todo item
  - ☐ Child todo item (indented only 2 spaces!)

✅ correctly aligned list marker

1. ☐ Parent todo item
   - ☐ Child todo item (indented 3 spaces!)

The CommonMark spec requires that nested list markers begin at the col of the first non-whitespace content after the parent list marker (which will be a different col for bullet list vs ordered list markers)

If you feel comfortable with the nuances of Markdown list syntax, you can disable the linter (default is enabled) via config:

{
  linter = {
    enabled = false
  }
}

☑️ Roadmap

Planned features:

  • Metadata support - mappings for quick addition of metadata/tags such as @start, @done, @due, @priority, etc. with custom highlighting. Added v0.2.0

  • Sub-task counter - add a completed/total count (e.g. 1/4) to parent todo items. Added v0.3.0

  • Archiving - manually or automatically move completed items to the bottom of the document. Added v0.7.0

  • Smart toggling - toggle all children checked if a parent todo is checked. Toggle a parent checked if the last unchecked child is checked. Added v0.7.0

  • Metadata upgrade - callbacks, async support, jump to. Added v0.9.0

  • Custom todo states - support beyond binary "checked" and "unchecked", allowing for todos to be in custom states, e.g. pending, not-planned, on-hold, etc. Added v0.10.0

  • List (todo) continuation - automatically created new todo lines in insert mode, e.g. <CR> on a todo line will create a new todo below. Added v0.11.0

☑️ Contributing

If you have feature suggestions or ideas, please feel free to open an issue on GitHub!

☑️ Credits