A Neovim sidebar that lists the files your branch changed and, under each one, the functions, classes and methods those changes touched.
- Compares against the branch you created yours from, so a stacked branch shows only its own changes, with or without a PR.
- Previews each change as you move through the tree, and steps through the changes from any window.
- Keeps review comments on lines or whole files, then pastes them into an AI agent's prompt or copies them as text.
- Points gitsigns at the sidebar's base: gitsigns' gutter and inline diff show the whole branch, not only uncommitted work.
Neovim 0.12 or newer, and git. Everything else is optional:
| Optional | Adds |
|---|---|
| A language server for the file's language | The symbol rows under each file. Without one, a file lists only its changes. |
| Treesitter parsers | Comment-only changes listed under Docs, and Rust and TypeScript tests found by their syntax. |
| gitsigns.nvim | The gutter's base, which marks the whole branch in the gutter, the unified diff, and the change colours. |
| herdr, a terminal multiplexer for coding agents | :Changeset review submit, which pastes your review into an agent's prompt. |
gh |
The open PR's target as the base, for a branch with no parent. |
| mini.icons or nvim-web-devicons | Icons. |
| mini.pick | :Pick changeset, and the base pickers' preview. Without it, the pickers use vim.ui.select. |
| which-key.nvim | ? opens its popup; version 3 also names the default keys' groups. |
:checkhealth changeset shows which of these it finds.
Each example also installs the optional plugins from the table above. Drop any you don't want.
With vim.pack:
vim.pack.add({ { src = "https://github.com/macintacos/changeset.nvim", version = vim.version.range("*") } })
-- Optional. Call each one's `setup()` somewhere in your config, such as
-- `require("gitsigns").setup()`.
vim.pack.add({
"https://github.com/lewis6991/gitsigns.nvim",
"https://github.com/nvim-mini/mini.icons",
"https://github.com/nvim-mini/mini.pick",
"https://github.com/folke/which-key.nvim",
})With lazy.nvim:
{
"macintacos/changeset.nvim",
version = "*",
dependencies = {
{ "lewis6991/gitsigns.nvim", opts = {} },
{ "nvim-mini/mini.icons", opts = {} },
{ "nvim-mini/mini.pick", opts = {} },
{ "folke/which-key.nvim", opts = {} },
},
}version installs the latest release; drop it to track main, which gets unreleased changes.
setup() is optional. To change an option, call require("changeset").setup({ … }), or add
opts = { … } to the lazy.nvim spec. The plugin loads lazily by itself: at startup it
defines :Changeset and its keys, and once gitsigns attaches a buffer it measures the
branch's base, running git and gh, to point gitsigns' gutter at it.
- On a branch with changes, run
:Changesetor press<C-g>g. The sidebar opens on the right. - Move with
jandk. Each row previews in the window you came from. - Press
<CR>to open the change, or<C-v>,<C-x>or<C-t>to open it in a split or a tab. - From any file,
<C-g>nnand<C-g>npopen the next and previous change.]gand[gmove through the sidebar's rows and preview each without opening it. - Press
?in the sidebar to list its keys, andqto close it.
- Press
<C-g>ccon a line or a visual selection. A small markdown window opens under it. It works on a row in the sidebar too: a symbol's row comments on the line<CR>opens, a change's row on its lines, a file's row on the whole file. On a file's first line the comment covers the whole file; to comment on that line alone, select it first. - Write the comment, then save it with
<C-s>or<C-CR>. Closing the window any other way keeps your text as a draft, which submit leaves out.<C-g>chholds a saved comment back as a draft, or saves a draft. - Walk your comments with
<C-g>cnand<C-g>cp, or list them with<C-g>cq. - When you're done,
<C-g>spastes the saved comments into an AI agent's prompt and takes them out of the review; drafts stay. Submit as often as you like: each submit sends only what you saved since the last one. This needs Neovim running in a herdr pane. Without herdr,<C-g>ycopies the comments as text and keeps them. - If a paste gets lost,
:Changeset review restorebrings a submitted batch back. The branch keeps its last 10.<C-g>aabandons the review.
Comments belong to the branch they were written on. They follow your edits each time you
write the file, and they stay on this machine. :help changeset-review-comments covers
drafts, blocks, hover and the rest.
:Changeset alone opens or focuses the sidebar, and closes it from inside. Each subcommand
has a <Plug>(changeset-…) map named after its words, and these default keys:
| Key | :Changeset … |
Does |
|---|---|---|
<C-g>g |
toggle |
Open, focus or close the sidebar |
<C-g>j |
pick |
Pick a change to open from a list |
<C-g>bb |
base [{ref}] |
Pick a branch to compare against, or name any ref |
<C-g>bt |
base tag |
Pick a tag to compare against |
<C-g>bc |
base commit |
Pick a commit to compare against |
<C-g>br |
base reset |
Compare against the base changeset guesses again |
<C-g>d |
diff |
Turn the unified diff on or off |
<C-g>r |
refresh |
Rebuild the tree |
<C-g>nn / <C-g>np |
next / prev |
Open the next / previous change |
<C-g>ns / <C-g>nS |
next symbol / prev symbol |
Open the next / previous changed symbol |
<C-g>nf / <C-g>nF |
next file / prev file |
Open the next / previous changed file |
]g / [g |
preview next / preview prev |
Preview the next / previous row |
<C-g>cc |
comment new |
Comment on the line or selection, or edit the one there |
<C-g>cd |
comment del |
Delete the comment on this line |
<C-g>ch |
comment draft |
Hold this line's comment back as a draft, or save it |
<C-g>cn / <C-g>cp |
comment next / comment prev |
Jump to the next / previous comment |
<C-g>cl |
comment last |
Edit the comment you saved last |
<C-g>cq |
comment list |
List the comments in the quickfix list |
<C-g>ct |
comment toggle |
Show or hide every comment's whole text |
<C-g>s |
review submit |
Paste the review into an agent's prompt |
<C-g>y |
review yank |
Copy the review as text |
<C-g>a |
review abandon |
Delete this branch's comments |
review restore |
Bring back a batch of submitted comments |
-
A default key that clashes with one you've mapped is left alone.
-
Set
vim.g.changeset_no_default_maps = trueto map none of them, then map the<Plug>maps you want:vim.g.changeset_no_default_maps = true vim.keymap.set("n", "<leader>gp", "<Plug>(changeset-toggle)") vim.keymap.set({ "n", "x" }, "<leader>c", "<Plug>(changeset-comment-new)")
-
The
<C-g>nkeys,<C-g>cnand<C-g>cptake a count and repeat with.. -
With the default keys mapped, Neovim's own
<C-g>(file info) waits'timeoutlen'for a second key.
| Key | keymaps option |
Does |
|---|---|---|
<CR> / double-click |
jump |
Open the row's change and focus it |
<S-CR> |
jump_close |
Open it and close the sidebar |
<C-v> / <C-x> / <C-t> |
jump_vsplit / jump_split / jump_tab |
Open it in a vertical split / split / tab |
q |
close |
Close the sidebar |
l / h |
expand / collapse |
Expand / collapse the row, or step out to its parent |
L / H |
expand_all / collapse_all |
Expand / collapse every file |
]] / [[ |
next_section / prev_section |
Move to the next / previous section |
f |
filter |
Filter the tree as you type |
F |
filter_kinds |
Hide symbol kinds, remembered for this branch, repository or everywhere |
y |
yank |
Copy the row's path:line, or a ranged comment's path:first-last |
x |
toggle_viewed |
Mark the file viewed, or not, until its diff changes |
X |
toggle_viewed_all |
Mark every file viewed, or none |
d |
delete_comment |
Delete the review comment on a Comments row |
R |
refresh |
Rebuild the tree |
? |
help |
List the sidebar's keys |
Set a key to false to leave it unbound. j and k step over section headers, stopping only on
a folded one. / searches the tree.
The tree shows what changed since your branch left the branch you created it from, else
its open PR's target (found with gh), else the default branch. On the default branch it
shows what you haven't pushed. The header names that base; :help changeset-base has the
full rules.
When that guess is wrong, pick what to compare against instead: a branch with <C-g>bb, a tag
with <C-g>bt, or one of your commits with <C-g>bc. With mini.pick, a preview beside the
list shows the files and commits the sidebar would hold against each one. :Changeset base <ref> takes any ref. changeset remembers the choice for the branch until <C-g>br.
Both need gitsigns.
- The gutter's base makes gitsigns' gutter mark everything the branch changed, not only
uncommitted work. It is the sidebar's base on every branch, the default branch included,
from the start of the session, so
setup()is not needed. It follows the base as you switch branches, pull or rebase, and after a push or fetch once Neovim regains focus,HEADnext moves or the sidebar reopens. An open sidebar moves with it.:Gitsigns change_base <rev>still sets a buffer's base by hand, and changeset leaves that buffer alone. - The unified diff turns on when the sidebar opens: every file the sidebar lists shows
its removed lines inline, above the lines that replaced them. It compares against the same
base as the gutter. Closing the sidebar turns it off, or, as
unified_diff.keepsays, keeps it on for the files you entered (opened from the sidebar, a step or the picker) or for all of them.<C-g>dturns it off or on at any time, and:Gitsigns diffthis unified=truecloses one file's view until it next turns on.
setup() is optional. These are the defaults:
require("changeset").setup({
-- The sidebar's keys, as listed above. Set one to false to leave it unbound.
keymaps = {
jump = "<CR>",
jump_close = "<S-CR>",
jump_vsplit = "<C-v>",
jump_split = "<C-x>",
jump_tab = "<C-t>",
close = "q",
expand = "l",
collapse = "h",
collapse_all = "H",
expand_all = "L",
next_section = "]]",
prev_section = "[[",
refresh = "R",
yank = "y",
toggle_viewed = "x",
toggle_viewed_all = "X",
delete_comment = "d",
help = "?",
filter_kinds = "F",
filter = "f",
},
layout = {
-- Below this many columns left for the files, the sidebar opens along the bottom.
min_file_width = 80,
},
review = {
-- Text pasted above the review, such as a skill to invoke.
header = "",
-- Text pasted below the review, such as standing instructions.
footer = "",
},
review_comment = {
-- The keys that save a review comment, in Insert and Normal mode.
save = { "<C-CR>", "<C-s>" },
-- The bubble in the sign column; false when your 'statuscolumn' draws it.
sign = true,
-- Start each session showing comments' whole text in blocks.
blocks = false,
},
unified_diff = {
-- Once the sidebar closes, keep the diff on "none", the "entered" files or "all".
keep = "none",
},
})Each call starts from the defaults. An unknown option is ignored with a warning, and an
invalid value raises an error naming the option. :help changeset-options describes each
one, and :help changeset-highlights lists the highlight groups.
:help changeset.nvimis the complete reference: every command, key, option and highlight group.:checkhealth changesetreports the requirements, the optional integrations and the options in force.- Contributors: doc/agents/design.md records why the sidebar looks and behaves as it does.
