From eddeae773b34791171735228ba7d3bb5fd4cc8a1 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Mar 2026 15:53:08 -0400 Subject: [PATCH 1/8] feat(commands): add `:Greview` for unified diff review with qflist/loclist Problem: diffs.nvim provides `:Gdiff` for single-file diffs and `gdiff_section()` for staged/unstaged sections, but has no way to view a full unified diff against an arbitrary git ref with structured navigation. Solution: add `M.greview(base, opts)` which creates a unified diff buffer (`diffs://review:{base}`), parses file/hunk positions, and populates the qflist (files with `+N`/`-M` stats) and loclist (hunks with filename and `@@` header). Add `M.review_file_at_line()` utility, `read_buffer()` review label case, and `:Greview` command with git ref tab completion. --- doc/diffs.nvim.txt | 41 ++++++++ lua/diffs/commands.lua | 215 ++++++++++++++++++++++++++++++++++++++ spec/commands_spec.lua | 73 +++++++++++++ spec/read_buffer_spec.lua | 29 +++++ 4 files changed, 358 insertions(+) diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index 07f818e..1094294 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -26,6 +26,7 @@ Features: ~ - Optional diff prefix (`+`/`-`/` `) concealment - Gutter (line number) highlighting - |:Gdiff| unified diff against any revision +- |:Greview| full-repo review diff with qflist/loclist navigation - Email quoting/patch syntax support (`> diff ...`) ============================================================================== @@ -449,6 +450,46 @@ COMMANDS *diffs-commands* :Ghdiff [revision] *:Ghdiff* Like |:Gdiff| but explicitly opens in a horizontal split. +:Greview {base} *:Greview* + Open a unified diff of the entire repository against {base} (a git ref + such as `origin/main`, `HEAD~5`, or a commit SHA). Displays in a + horizontal split. + + Populates the quickfix list with file entries (one per changed file, + with `+N`/`-M` stats) and the location list with hunk entries (one per + `@@` header, with filename and hunk number). All entries point into the + diff buffer, so |:cnext|/|:cprev| navigate between files and + |:lnext|/|:lprev| navigate between hunks within the buffer. + + The buffer is named `diffs://review:{base}`. Reloading with |:edit| + refreshes the diff content. + + If a `diffs://` window already exists in the current tabpage, the new + diff replaces its buffer instead of creating another split. + + Parameters: ~ + {base} (string) Git ref to diff against. Required. + + Examples: >vim + :Greview origin/main + :Greview HEAD~10 + :Greview abc1234 +< + + Lua API: >lua + require('diffs.commands').greview('origin/main') + require('diffs.commands').greview('origin/main', { vertical = true }) + require('diffs.commands').greview('origin/main', { repo_root = '/path/to/repo' }) +< + When called from async contexts (e.g., plugin callbacks where the + current buffer may not be in a git repo), pass `repo_root` explicitly + to avoid repo detection failures. + + Utilities: ~ + `require('diffs.commands').review_file_at_line(bufnr, lnum)` returns + the filename at a given line in a review buffer by walking backwards + to the nearest `diff --git` header. + ============================================================================== MAPPINGS *diffs-mappings* diff --git a/lua/diffs/commands.lua b/lua/diffs/commands.lua index 01f1d3a..31840bc 100644 --- a/lua/diffs/commands.lua +++ b/lua/diffs/commands.lua @@ -362,6 +362,188 @@ function M.gdiff_section(repo_root, opts) end) end +---@class diffs.GreviewOpts +---@field vertical? boolean +---@field repo_root? string + +---@param base string +---@param opts? diffs.GreviewOpts +---@return integer? +function M.greview(base, opts) + opts = opts or {} + + if not base or base == '' then + vim.notify('[diffs.nvim]: greview requires a base ref', vim.log.levels.ERROR) + return nil + end + + local repo_root = opts.repo_root + if not repo_root then + local bufnr = vim.api.nvim_get_current_buf() + local filepath = vim.api.nvim_buf_get_name(bufnr) + repo_root = git.get_repo_root(filepath ~= '' and filepath or nil) + end + if not repo_root then + local cwd = vim.fn.getcwd() + repo_root = git.get_repo_root(cwd .. '/.') + end + if not repo_root then + vim.notify('[diffs.nvim]: not in a git repository', vim.log.levels.ERROR) + return nil + end + + local target_name = 'diffs://review:' .. base + local existing_buf = vim.fn.bufnr(target_name) + if existing_buf ~= -1 then + pcall(vim.api.nvim_buf_delete, existing_buf, { force = true }) + end + + local cmd = { 'git', '-C', repo_root, 'diff', '--no-ext-diff', '--no-color', base } + local result = vim.fn.systemlist(cmd) + if vim.v.shell_error ~= 0 then + vim.notify('[diffs.nvim]: git diff failed', vim.log.levels.ERROR) + return nil + end + result = replace_combined_diffs(result, repo_root) + if #result == 0 then + vim.notify('[diffs.nvim]: no diff against ' .. base, vim.log.levels.INFO) + return nil + end + + local diff_buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, result) + vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = diff_buf }) + vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = diff_buf }) + vim.api.nvim_set_option_value('swapfile', false, { buf = diff_buf }) + vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf }) + vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf }) + vim.api.nvim_buf_set_name(diff_buf, 'diffs://review:' .. base) + vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root) + + local qf_items = {} + local loc_items = {} + local current_file = nil + local file_adds, file_dels = {}, {} + local file_hunk_count = {} + + for i, line in ipairs(result) do + local file = line:match('^diff %-%-git a/.+ b/(.+)$') + if file then + current_file = file + file_adds[file] = 0 + file_dels[file] = 0 + file_hunk_count[file] = 0 + table.insert(qf_items, { + bufnr = diff_buf, + lnum = i, + text = file, + }) + elseif current_file and line:match('^@@') then + file_hunk_count[current_file] = file_hunk_count[current_file] + 1 + table.insert(loc_items, { + bufnr = diff_buf, + lnum = i, + text = current_file, + _hunk = file_hunk_count[current_file], + _header = line:match('^(@@.-@@)') or '', + }) + elseif current_file then + local ch = line:sub(1, 1) + if ch == '+' and not line:match('^%+%+%+') then + file_adds[current_file] = file_adds[current_file] + 1 + elseif ch == '-' and not line:match('^%-%-%-') then + file_dels[current_file] = file_dels[current_file] + 1 + end + end + end + + local max_fname = 0 + local max_add, max_del = 0, 0 + for _, item in ipairs(qf_items) do + max_fname = math.max(max_fname, #item.text) + local a = file_adds[item.text] or 0 + local d = file_dels[item.text] or 0 + if a > 0 then + max_add = math.max(max_add, #tostring(a) + 1) + end + if d > 0 then + max_del = math.max(max_del, #tostring(d) + 1) + end + end + + for _, item in ipairs(qf_items) do + local file = item.text + local a = file_adds[file] or 0 + local d = file_dels[file] or 0 + local padded = file .. string.rep(' ', max_fname - #file) + local parts = { padded } + if max_add > 0 then + parts[#parts + 1] = a > 0 + and string.format('%' .. max_add .. 's', '+' .. a) + or string.rep(' ', max_add) + end + if max_del > 0 then + parts[#parts + 1] = d > 0 + and string.format('%' .. max_del .. 's', '-' .. d) + or string.rep(' ', max_del) + end + item.text = table.concat(parts, ' ') + end + + local max_loc_fname = 0 + for _, item in ipairs(loc_items) do + max_loc_fname = math.max(max_loc_fname, #item.text) + end + for _, item in ipairs(loc_items) do + item.text = item.text + .. string.rep(' ', max_loc_fname - #item.text) + .. ' (hunk ' .. item._hunk .. ') ' .. item._header + item._hunk = nil + item._header = nil + end + + vim.fn.setqflist({}, ' ', { + title = 'review: ' .. base, + items = qf_items, + }) + vim.fn.setloclist(0, {}, ' ', { + title = 'review hunks: ' .. base, + items = loc_items, + }) + + local existing_win = M.find_diffs_window() + if existing_win then + vim.api.nvim_set_current_win(existing_win) + vim.api.nvim_win_set_buf(existing_win, diff_buf) + else + vim.cmd(opts.vertical and 'vsplit' or 'split') + vim.api.nvim_win_set_buf(0, diff_buf) + end + + M.setup_diff_buf(diff_buf) + dbg('opened review buffer %d against %s', diff_buf, base) + + vim.schedule(function() + require('diffs').attach(diff_buf) + end) + + return diff_buf +end + +---@param buf integer +---@param lnum integer +---@return string? +function M.review_file_at_line(buf, lnum) + local lines = vim.api.nvim_buf_get_lines(buf, 0, lnum, false) + for i = #lines, 1, -1 do + local file = lines[i]:match('^diff %-%-git a/.+ b/(.+)$') + if file then + return file + end + end + return nil +end + ---@param bufnr integer function M.read_buffer(bufnr) local name = vim.api.nvim_buf_get_name(bufnr) @@ -392,6 +574,13 @@ function M.read_buffer(bufnr) diff_lines = {} end + diff_lines = replace_combined_diffs(diff_lines, repo_root) + elseif label == 'review' then + local cmd = { 'git', '-C', repo_root, 'diff', '--no-ext-diff', '--no-color', path } + diff_lines = vim.fn.systemlist(cmd) + if vim.v.shell_error ~= 0 then + diff_lines = {} + end diff_lines = replace_combined_diffs(diff_lines, repo_root) else local abs_path = repo_root .. '/' .. path @@ -459,6 +648,32 @@ function M.setup() nargs = '?', desc = 'Show unified diff against git revision in horizontal split', }) + + vim.api.nvim_create_user_command('Greview', function(opts) + M.greview(opts.args ~= '' and opts.args or nil) + end, { + nargs = 1, + complete = function(arglead) + local refs = vim.fn.systemlist({ + 'git', 'for-each-ref', '--format=%(refname:short)', + 'refs/heads/', 'refs/remotes/', 'refs/tags/', + }) + if vim.v.shell_error ~= 0 then + return {} + end + if arglead == '' then + return refs + end + local matches = {} + for _, ref in ipairs(refs) do + if ref:find(arglead, 1, true) == 1 then + table.insert(matches, ref) + end + end + return matches + end, + desc = 'Show unified diff against a git ref with qflist/loclist', + }) end return M diff --git a/spec/commands_spec.lua b/spec/commands_spec.lua index 1bf9e61..01cddfb 100644 --- a/spec/commands_spec.lua +++ b/spec/commands_spec.lua @@ -112,6 +112,79 @@ describe('commands', function() end) end) + describe('setup registers Greview command', function() + it('registers Greview command', function() + commands.setup() + local cmds = vim.api.nvim_get_commands({}) + assert.is_not_nil(cmds.Greview) + end) + end) + + describe('review_file_at_line', function() + local test_buffers = {} + + after_each(function() + for _, bufnr in ipairs(test_buffers) do + if vim.api.nvim_buf_is_valid(bufnr) then + vim.api.nvim_buf_delete(bufnr, { force = true }) + end + end + test_buffers = {} + end) + + it('returns filename at cursor line', function() + local bufnr = vim.api.nvim_create_buf(false, true) + table.insert(test_buffers, bufnr) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'diff --git a/foo.lua b/foo.lua', + '--- a/foo.lua', + '+++ b/foo.lua', + '@@ -1,3 +1,4 @@', + ' local M = {}', + '+local x = 1', + ' return M', + }) + + assert.are.equal('foo.lua', commands.review_file_at_line(bufnr, 6)) + end) + + it('returns correct file in multi-file diff', function() + local bufnr = vim.api.nvim_create_buf(false, true) + table.insert(test_buffers, bufnr) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'diff --git a/foo.lua b/foo.lua', + '@@ -1 +1 @@', + '-old', + '+new', + 'diff --git a/bar.lua b/bar.lua', + '@@ -1 +1 @@', + '-old', + '+new', + }) + + assert.are.equal('foo.lua', commands.review_file_at_line(bufnr, 3)) + assert.are.equal('bar.lua', commands.review_file_at_line(bufnr, 7)) + end) + + it('returns nil before any diff header', function() + local bufnr = vim.api.nvim_create_buf(false, true) + table.insert(test_buffers, bufnr) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'some preamble text', + 'diff --git a/foo.lua b/foo.lua', + }) + + assert.is_nil(commands.review_file_at_line(bufnr, 1)) + end) + + it('returns nil on empty buffer', function() + local bufnr = vim.api.nvim_create_buf(false, true) + table.insert(test_buffers, bufnr) + + assert.is_nil(commands.review_file_at_line(bufnr, 1)) + end) + end) + describe('find_hunk_line', function() it('finds matching @@ header and returns target line', function() local diff_lines = { diff --git a/spec/read_buffer_spec.lua b/spec/read_buffer_spec.lua index f571d97..b94887f 100644 --- a/spec/read_buffer_spec.lua +++ b/spec/read_buffer_spec.lua @@ -286,6 +286,35 @@ describe('read_buffer', function() assert.is_truthy(vim.tbl_contains(captured_cmd, '--cached')) end) + + it('runs git diff with base ref for review label', function() + local captured_cmd + mock_systemlist(function(cmd) + captured_cmd = cmd + return { + 'diff --git a/file.lua b/file.lua', + '--- a/file.lua', + '+++ b/file.lua', + '@@ -1 +1 @@', + '-old', + '+new', + } + end) + + local bufnr = create_diffs_buffer('diffs://review:origin/main', { + diffs_repo_root = '/home/test/repo', + }) + commands.read_buffer(bufnr) + + assert.is_not_nil(captured_cmd) + assert.are.equal('git', captured_cmd[1]) + assert.are.equal('/home/test/repo', captured_cmd[3]) + assert.are.equal('diff', captured_cmd[4]) + assert.are.equal('origin/main', captured_cmd[#captured_cmd]) + + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + assert.are.equal('diff --git a/file.lua b/file.lua', lines[1]) + end) end) describe('content', function() From 9e15467815a3bd38847797127684ac3e10fa6401 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Mar 2026 16:34:42 -0400 Subject: [PATCH 2/8] feat: format --- doc/diffs.nvim.txt | 252 ++++++++++++++++++++++----------------------- 1 file changed, 121 insertions(+), 131 deletions(-) diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index 1094294..1b65539 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -1,13 +1,12 @@ *diffs.nvim.txt* Syntax highlighting for diffs in Neovim -Author: Barrett Ruth -License: MIT +Author: Barrett Ruth License: MIT ============================================================================== -INTRODUCTION *diffs.nvim* +INTRODUCTION *diffs.nvim* -diffs.nvim adds language-aware syntax highlighting to unified diff content -in Neovim buffers. It replaces flat `diffAdded`/`diffRemoved` coloring with +diffs.nvim adds language-aware syntax highlighting to unified diff content in +Neovim buffers. It replaces flat `diffAdded`/`diffRemoved` coloring with treesitter syntax, blended line backgrounds, and character-level intra-line diffs. @@ -66,14 +65,13 @@ Install with lazy.nvim: >lua { 'barrettruth/diffs.nvim' } < -Do not lazy load with `event`, `lazy`, `ft`, `config`, or `keys` — -diffs.nvim lazy-loads itself. +Do not lazy load with `event`, `lazy`, `ft`, `config`, or `keys` — diffs.nvim +lazy-loads itself. -NOTE: Load your colorscheme before diffs.nvim. With lazy.nvim, set -`priority = 1000` and `lazy = false` on your colorscheme plugin. +NOTE: Load your colorscheme before diffs.nvim. With lazy.nvim, set `priority = +1000` and `lazy = false` on your colorscheme plugin. -See |diffs-config| for customization, |diffs-integrations| for plugin -support. +See |diffs-config| for customization, |diffs-integrations| for plugin support. ============================================================================== CONFIGURATION *diffs-config* @@ -138,7 +136,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: }, } < - *diffs.Config* + *diffs.Config* Fields: ~ {debug} (boolean, default: false) Enable debug logging to |:messages| with @@ -156,7 +154,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: `false`, or a table with sub-options. Passing `true` or a table enables the integration; `false` disables it. See |diffs-integrations|. - *diffs.IntegrationsConfig* + *diffs.IntegrationsConfig* Fields: ~ {fugitive} (boolean|table, default: false) @@ -241,7 +239,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: Inline merge conflict resolution options. See |diffs.ConflictConfig| for fields. - *diffs.Highlights* + *diffs.Highlights* Highlights table fields: ~ {background} (boolean, default: true) Apply background highlighting to `+`/`-` lines @@ -293,7 +291,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: so overrides always win over both computed defaults and colorscheme definitions. - *diffs.ContextConfig* + *diffs.ContextConfig* Context config fields: ~ {enabled} (boolean, default: true) Read surrounding code from the working tree @@ -311,7 +309,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: Files are read once per parse and cached across hunks in the same file. - *diffs.PrioritiesConfig* + *diffs.PrioritiesConfig* Priorities config fields: ~ {clear} (integer, default: 198) Priority for `DiffsClear` extmarks that reset @@ -333,7 +331,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: character-level background extmarks. Highest priority so changed characters stand out. - *diffs.TreesitterConfig* + *diffs.TreesitterConfig* Treesitter config fields: ~ {enabled} (boolean, default: true) Apply treesitter syntax highlighting to code. @@ -344,7 +342,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: Context lines are not counted. Prevents lag on massive diffs. - *diffs.VimConfig* + *diffs.VimConfig* Vim config fields: ~ {enabled} (boolean, default: true) Use vim syntax highlighting as fallback when no @@ -363,7 +361,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: treesitter default due to the per-character cost of |synID()|. - *diffs.IntraConfig* + *diffs.IntraConfig* Intra config fields: ~ {enabled} (boolean, default: true) Enable character-level diff highlighting within @@ -409,20 +407,18 @@ To increase the threshold: >lua vim = { max_lines = 500 }, -- default: 200 }, } -< -To suppress the warning without changing the threshold: >lua +< To suppress the warning without changing the threshold: >lua vim.g.diffs = { highlights = { warn_max_lines = false }, } -< -The `intra.max_lines` threshold (default: 500) is separate and controls -character-level diff highlighting within changed lines. It does not affect -the syntax highlighting warning. +< The `intra.max_lines` threshold (default: 500) is separate and controls +character-level diff highlighting within changed lines. It does not affect the +syntax highlighting warning. ============================================================================== COMMANDS *diffs-commands* -:Gdiff [revision] *:Gdiff* +:Gdiff [revision] *:Gdiff* Open a unified diff of the current file against a git revision. Displays in a horizontal split below the current window. @@ -444,13 +440,13 @@ COMMANDS *diffs-commands* :Gdiff abc123 " diff against specific commit < -:Gvdiff [revision] *:Gvdiff* +:Gvdiff [revision] *:Gvdiff* Like |:Gdiff| but opens in a vertical split. -:Ghdiff [revision] *:Ghdiff* +:Ghdiff [revision] *:Ghdiff* Like |:Gdiff| but explicitly opens in a horizontal split. -:Greview {base} *:Greview* +:Greview {base} *:Greview* Open a unified diff of the entire repository against {base} (a git ref such as `origin/main`, `HEAD~5`, or a commit SHA). Displays in a horizontal split. @@ -491,13 +487,13 @@ COMMANDS *diffs-commands* to the nearest `diff --git` header. ============================================================================== -MAPPINGS *diffs-mappings* +MAPPINGS *diffs-mappings* - *(diffs-gdiff)* + *(diffs-gdiff)* (diffs-gdiff) Show unified diff against HEAD in a horizontal split. Equivalent to |:Gdiff| with no arguments. - *(diffs-gvdiff)* + *(diffs-gvdiff)* (diffs-gvdiff) Show unified diff against HEAD in a vertical split. Equivalent to |:Gvdiff| with no arguments. @@ -506,29 +502,29 @@ Example configuration: >lua vim.keymap.set('n', 'gD', '(diffs-gvdiff)') < - *(diffs-conflict-ours)* + *(diffs-conflict-ours)* (diffs-conflict-ours) Accept current (ours) change. Replaces the conflict block with ours content. - *(diffs-conflict-theirs)* + *(diffs-conflict-theirs)* (diffs-conflict-theirs) Accept incoming (theirs) change. Replaces the conflict block with theirs content. - *(diffs-conflict-both)* + *(diffs-conflict-both)* (diffs-conflict-both) Accept both changes (ours then theirs). - *(diffs-conflict-none)* + *(diffs-conflict-none)* (diffs-conflict-none) Reject both changes (delete entire block). - *(diffs-conflict-next)* + *(diffs-conflict-next)* (diffs-conflict-next) Jump to next conflict marker. Wraps around. - *(diffs-conflict-prev)* + *(diffs-conflict-prev)* (diffs-conflict-prev) Jump to previous conflict marker. Wraps around. @@ -541,47 +537,45 @@ Example configuration: >lua vim.keymap.set('n', '[c', '(diffs-conflict-prev)') < - *(diffs-merge-ours)* + *(diffs-merge-ours)* (diffs-merge-ours) Accept ours in a merge diff view. Resolves the conflict in the working file with ours content. - *(diffs-merge-theirs)* + *(diffs-merge-theirs)* (diffs-merge-theirs) Accept theirs in a merge diff view. - *(diffs-merge-both)* + *(diffs-merge-both)* (diffs-merge-both) Accept both (ours then theirs) in a merge diff view. - *(diffs-merge-none)* + *(diffs-merge-none)* (diffs-merge-none) Reject both in a merge diff view. - *(diffs-merge-next)* + *(diffs-merge-next)* (diffs-merge-next) Jump to next unresolved conflict hunk in merge diff. - *(diffs-merge-prev)* + *(diffs-merge-prev)* (diffs-merge-prev) Jump to previous unresolved conflict hunk in merge diff. Diff buffer mappings: ~ - *diffs-q* + *diffs-q* q Close the diff window. Available in all `diffs://` buffers created by |:Gdiff|, |:Gvdiff|, |:Ghdiff|, or the fugitive status keymaps. ============================================================================== -INTEGRATIONS *diffs-integrations* +INTEGRATIONS *diffs-integrations* -diffs.nvim integrates with several plugins. There are two attachment -patterns: +diffs.nvim integrates with several plugins. There are two attachment patterns: -Automatic: ~ -Enable via config toggles. The plugin registers `FileType` autocmds for -each integration's filetypes and attaches automatically. +Automatic: ~ Enable via config toggles. The plugin registers `FileType` +autocmds for each integration's filetypes and attaches automatically. >lua vim.g.diffs = { integrations = { @@ -593,9 +587,8 @@ each integration's filetypes and attaches automatically. } < -Opt-in: ~ -For filetypes not covered by a built-in integration, use `extra_filetypes` -to attach to any buffer whose content looks like a diff. +Opt-in: ~ For filetypes not covered by a built-in integration, use +`extra_filetypes` to attach to any buffer whose content looks like a diff. >lua vim.g.diffs = { extra_filetypes = { 'diff' } } < @@ -608,8 +601,8 @@ Enable vim-fugitive (https://github.com/tpope/vim-fugitive) support: >lua < |:Git| status and commit views receive treesitter syntax, line backgrounds, -and intra-line diffs. |:Gdiff| opens a unified diff against any revision -(see |diffs-commands|). +and intra-line diffs. |:Gdiff| opens a unified diff against any revision (see +|diffs-commands|). Fugitive status keymaps: ~ @@ -617,7 +610,7 @@ When inside a |:Git| status buffer, diffs.nvim provides keymaps to open unified diffs for files or entire sections. Keymaps: ~ - *diffs-du* *diffs-dU* + *diffs-du* *diffs-dU* du Open unified diff in a horizontal split. dU Open unified diff in a vertical split. @@ -642,7 +635,7 @@ staged) and displays all changes in that section as a single unified diff. Untracked section headers show a warning since there is no meaningful diff. Configuration: ~ - *diffs.FugitiveConfig* + *diffs.FugitiveConfig* >lua vim.g.diffs = { integrations = { @@ -672,9 +665,8 @@ Enable Neogit (https://github.com/NeogitOrg/neogit) support: >lua vim.g.diffs = { integrations = { neogit = true } } < -Expanding a diff in a Neogit buffer (e.g., TAB on a file in the status -view) applies treesitter syntax highlighting and intra-line diffs to the -hunk lines. +Expanding a diff in a Neogit buffer (e.g., TAB on a file in the status view) +applies treesitter syntax highlighting and intra-line diffs to the hunk lines. ------------------------------------------------------------------------------ NEOJJ *diffs-neojj* @@ -683,15 +675,14 @@ Enable neojj (https://github.com/NicholasZolton/neojj) support: >lua vim.g.diffs = { integrations = { neojj = true } } < -Expanding a diff in a neojj buffer (e.g., TAB on a file in the status -view) applies treesitter syntax highlighting and intra-line diffs to the -hunk lines. +Expanding a diff in a neojj buffer (e.g., TAB on a file in the status view) +applies treesitter syntax highlighting and intra-line diffs to the hunk lines. ------------------------------------------------------------------------------ GITSIGNS *diffs-gitsigns* -Enable gitsigns.nvim (https://github.com/lewis6991/gitsigns.nvim) blame -popup highlighting: >lua +Enable gitsigns.nvim (https://github.com/lewis6991/gitsigns.nvim) blame popup +highlighting: >lua vim.g.diffs = { integrations = { gitsigns = true } } < @@ -702,7 +693,7 @@ Highlights are applied in a separate `diffs-gitsigns` namespace and do not interfere with the main decoration provider used for diff buffers. ------------------------------------------------------------------------------ -TELESCOPE *diffs-telescope* +TELESCOPE *diffs-telescope* Enable telescope.nvim (https://github.com/nvim-telescope/telescope.nvim) preview highlighting: >lua @@ -711,31 +702,31 @@ preview highlighting: >lua Telescope does not set `filetype=diff` on preview buffers — it calls `vim.treesitter.start(bufnr, "diff")` directly, so diffs.nvim's `FileType` -autocmd never fires. This integration listens for the -`User TelescopePreviewerLoaded` event and attaches to the preview buffer. +autocmd never fires. This integration listens for the `User +TelescopePreviewerLoaded` event and attaches to the preview buffer. Pickers that show diff content (e.g. `git_bcommits`, `git_status`) will receive treesitter syntax, line backgrounds, and intra-line diffs in the preview pane. Known issue: Telescope's previewer may render the first line of the preview -buffer with a black background regardless of colorscheme. This is a -Telescope artifact unrelated to diffs.nvim. Tracked upstream: +buffer with a black background regardless of colorscheme. This is a Telescope +artifact unrelated to diffs.nvim. Tracked upstream: https://github.com/nvim-telescope/telescope.nvim/issues/3626 ============================================================================== -CONFLICT RESOLUTION *diffs-conflict* +CONFLICT RESOLUTION *diffs-conflict* diffs.nvim detects inline merge conflict markers (`<<<<<<<`/`=======`/ `>>>>>>>`) in working files and provides highlighting and resolution keymaps. Both standard and diff3 (`|||||||`) formats are supported. -Conflict regions are detected automatically on `BufReadPost` and re-scanned -on `TextChanged`. When all conflicts in a buffer are resolved, highlighting -is removed and diagnostics are re-enabled. +Conflict regions are detected automatically on `BufReadPost` and re-scanned on +`TextChanged`. When all conflicts in a buffer are resolved, highlighting is +removed and diagnostics are re-enabled. Configuration: ~ - *diffs.ConflictConfig* + *diffs.ConflictConfig* >lua vim.g.diffs = { conflict = { @@ -807,7 +798,7 @@ Configuration: ~ and navigation. Each value accepts a string (custom key) or `false` (disabled). - *diffs.ConflictKeymaps* + *diffs.ConflictKeymaps* Keymap fields: ~ {ours} (string|false, default: 'doo') Accept current (ours) change. @@ -829,7 +820,7 @@ Configuration: ~ around. User events: ~ - *DiffsConflictResolved* + *DiffsConflictResolved* DiffsConflictResolved Fired when the last conflict in a buffer is resolved. Useful for triggering custom actions (e.g., auto-staging the file). >lua @@ -842,15 +833,15 @@ User events: ~ < ============================================================================== -MERGE DIFF RESOLUTION *diffs-merge* +MERGE DIFF RESOLUTION *diffs-merge* When pressing `du`/`dU` on an unmerged (`U`) file in the fugitive status -buffer, diffs.nvim opens a unified diff of ours (`git show :2:path`) vs -theirs (`git show :3:path`) with full treesitter and intra-line highlighting. +buffer, diffs.nvim opens a unified diff of ours (`git show :2:path`) vs theirs +(`git show :3:path`) with full treesitter and intra-line highlighting. -The same conflict resolution keymaps (`doo`/`dot`/`dob`/`don`/`]c`/`[c`) -are available on the diff buffer. They resolve conflicts in the working -file by matching diff hunks to conflict markers: +The same conflict resolution keymaps (`doo`/`dot`/`dob`/`don`/`]c`/`[c`) are +available on the diff buffer. They resolve conflicts in the working file by +matching diff hunks to conflict markers: - `doo` replaces the conflict region with ours content - `dot` replaces the conflict region with theirs content @@ -859,24 +850,24 @@ file by matching diff hunks to conflict markers: - `]c`/`[c` navigate between unresolved conflict hunks Resolved hunks are marked with `(resolved)` virtual text. Hunks that -correspond to auto-merged content (no conflict markers) show an -informational notification and are left unchanged. +correspond to auto-merged content (no conflict markers) show an informational +notification and are left unchanged. -The working file buffer is modified in place; save it when ready. -Phase 1 inline conflict highlights (see |diffs-conflict|) are refreshed -automatically after each resolution. +The working file buffer is modified in place; save it when ready. Phase 1 +inline conflict highlights (see |diffs-conflict|) are refreshed automatically +after each resolution. ============================================================================== API *diffs-api* -attach({bufnr}) *diffs.attach()* +attach({bufnr}) *diffs.attach()* Manually attach highlighting to a buffer. Called automatically for fugitive buffers via the `FileType fugitive` autocmd. Parameters: ~ {bufnr} (integer, optional) Buffer number. Defaults to current buffer. -refresh({bufnr}) *diffs.refresh()* +refresh({bufnr}) *diffs.refresh()* Manually refresh highlighting for a buffer. Useful after external changes or for debugging. @@ -884,7 +875,7 @@ refresh({bufnr}) *diffs.refresh()* {bufnr} (integer, optional) Buffer number. Defaults to current buffer. ============================================================================== -IMPLEMENTATION *diffs-implementation* +IMPLEMENTATION *diffs-implementation* Summary / commit detail views: ~ 1. `FileType` autocmd for computed filetypes (see |diffs-config|) triggers @@ -914,31 +905,31 @@ Diff mode views: ~ show through the diff colors ============================================================================== -KNOWN LIMITATIONS *diffs-limitations* +KNOWN LIMITATIONS *diffs-limitations* Incomplete Syntax Context ~ - *diffs-syntax-context* + *diffs-syntax-context* Treesitter parses each diff hunk in isolation. When `highlights.context` is -enabled (the default), surrounding code is read from the working tree file -and fed into the parser to improve accuracy at hunk boundaries. This helps -when a hunk is inside a table, function body, or loop whose opening is -beyond the hunk's own context lines. Requires `repo_root` and -`file_new_start` to be available on the hunk (true for standard unified -diffs). In rare cases, hunks that start or end mid-expression may still -produce imperfect highlights due to treesitter error recovery. +enabled (the default), surrounding code is read from the working tree file and +fed into the parser to improve accuracy at hunk boundaries. This helps when a +hunk is inside a table, function body, or loop whose opening is beyond the +hunk's own context lines. Requires `repo_root` and `file_new_start` to be +available on the hunk (true for standard unified diffs). In rare cases, hunks +that start or end mid-expression may still produce imperfect highlights due to +treesitter error recovery. Syntax Highlighting Flash ~ - *diffs-flash* + *diffs-flash* When opening a fugitive buffer, there is an unavoidable visual "flash" where the buffer briefly shows fugitive's default diff highlighting before diffs.nvim applies treesitter highlights. -This occurs because diffs.nvim hooks into the `FileType fugitive` event, -which fires after vim-fugitive has already painted the buffer. The -decoration provider applies highlights on the next redraw cycle. +This occurs because diffs.nvim hooks into the `FileType fugitive` event, which +fires after vim-fugitive has already painted the buffer. The decoration +provider applies highlights on the next redraw cycle. Conflicting Diff Plugins ~ - *diffs-plugin-conflicts* + *diffs-plugin-conflicts* diffs.nvim may not interact well with other plugins that modify diff highlighting or the sign column in diff views. Known plugins that may conflict: @@ -965,7 +956,7 @@ If you experience visual conflicts, try disabling the conflicting plugin's diff-related features. ============================================================================== -HIGHLIGHT GROUPS *diffs-highlights* +HIGHLIGHT GROUPS *diffs-highlights* diffs.nvim defines custom highlight groups. All groups use `default = true`, so colorschemes can override them by defining the group before the plugin @@ -979,93 +970,92 @@ character-level blend. Fugitive unified diff highlights: ~ - *DiffsAdd* + *DiffsAdd* DiffsAdd Background for `+` lines. Derived by blending `DiffAdd` background with `Normal` at 40% alpha. - *DiffsDelete* + *DiffsDelete* DiffsDelete Background for `-` lines. Derived by blending `DiffDelete` background with `Normal` at 40% alpha. - *DiffsAddNr* + *DiffsAddNr* DiffsAddNr Line number for `+` lines. Foreground from `DiffsAddText`, background from `DiffsAdd`. - *DiffsDeleteNr* + *DiffsDeleteNr* DiffsDeleteNr Line number for `-` lines. Foreground from `DiffsDeleteText`, background from `DiffsDelete`. - *DiffsAddText* + *DiffsAddText* DiffsAddText Character-level background for changed characters within `+` lines. Uses the raw `DiffAdd` background color. Only sets `bg`, so treesitter foreground colors show through. - *DiffsDeleteText* + *DiffsDeleteText* DiffsDeleteText Character-level background for changed characters within `-` lines. Uses the raw `DiffDelete` background color. Conflict highlights: ~ - *DiffsConflictOurs* + *DiffsConflictOurs* DiffsConflictOurs Background for "ours" (current) content lines. Derived by blending `DiffAdd` background with `Normal` at 40% alpha (green tint). - *DiffsConflictTheirs* + *DiffsConflictTheirs* DiffsConflictTheirs Background for "theirs" (incoming) content lines. Derived by blending `DiffChange` background with `Normal` at 40% alpha. - *DiffsConflictBase* + *DiffsConflictBase* DiffsConflictBase Background for base (ancestor) content lines in diff3 conflicts. Derived by blending `DiffText` background with `Normal` at 30% alpha (muted). - *DiffsConflictMarker* + *DiffsConflictMarker* DiffsConflictMarker Dimmed foreground with bold for `<<<<<<<`, `=======`, `>>>>>>>`, and `|||||||` marker lines. - *DiffsConflictOursNr* + *DiffsConflictOursNr* DiffsConflictOursNr Line number for "ours" content lines. Foreground from higher-alpha blend, background from line-level blend. - *DiffsConflictTheirsNr* + *DiffsConflictTheirsNr* DiffsConflictTheirsNr Line number for "theirs" content lines. - *DiffsConflictBaseNr* + *DiffsConflictBaseNr* DiffsConflictBaseNr Line number for base content lines (diff3). - *DiffsConflictActions* + *DiffsConflictActions* DiffsConflictActions Dimmed foreground (no bold) for the codelens-style action line shown when `show_actions` is true. -Diff mode window highlights: ~ -These are used for |winhighlight| remapping in `&diff` windows. +Diff mode window highlights: ~ These are used for |winhighlight| remapping in +`&diff` windows. - *DiffsDiffAdd* + *DiffsDiffAdd* DiffsDiffAdd Background-only. Derived from `DiffAdd` bg. Treesitter provides foreground syntax highlighting. - *DiffsDiffDelete* + *DiffsDiffDelete* DiffsDiffDelete Foreground and background from `DiffDelete`. Used for filler lines (`/////`) which have no real code content to highlight. - *DiffsDiffChange* + *DiffsDiffChange* DiffsDiffChange Background-only. Derived from `DiffChange` bg. Treesitter provides foreground syntax highlighting. - *DiffsDiffText* + *DiffsDiffText* DiffsDiffText Background-only. Derived from `DiffText` bg. Treesitter provides foreground syntax highlighting. To customize these in your colorscheme: >lua vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = '#2e4a3a' }) vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { link = 'DiffDelete' }) -< -Or via `highlights.overrides` in config: >lua +< Or via `highlights.overrides` in config: >lua vim.g.diffs = { highlights = { overrides = { @@ -1077,7 +1067,7 @@ Or via `highlights.overrides` in config: >lua < ============================================================================== -HEALTH CHECK *diffs-health* +HEALTH CHECK *diffs-health* Run |:checkhealth| diffs to verify your setup. @@ -1087,7 +1077,7 @@ Checks performed: - libvscode_diff shared library is available (optional) ============================================================================== -ACKNOWLEDGEMENTS *diffs-acknowledgements* +ACKNOWLEDGEMENTS *diffs-acknowledgements* - vim-fugitive (https://github.com/tpope/vim-fugitive) - codediff.nvim (https://github.com/esmuellert/codediff.nvim) From df6e3ff6ae89778d6ce90f47968994362e378247 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Mar 2026 17:02:53 -0400 Subject: [PATCH 3/8] fix: resolve CI quality check failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: three CI jobs failing — stylua format, lua type check (`param-type-mismatch` on `get_repo_root`), and vimdoc unresolved tags for vim builtins and external plugin references. Solution: fix `get_repo_root` param annotation to `string?`, use `nix develop .#ci` in all workflow commands, wrap `vimdoc-language-server` with `--runtime-tags` in the ci devShell, and fix external tag references (`:Git` → backtick, `winhighlight` → option syntax). --- .github/workflows/quality.yaml | 8 ++++---- flake.nix | 9 ++++++--- lua/diffs/git.lua | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 0391357..303519f 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v31 - - run: nix develop --command stylua --check . + - run: nix develop .#ci --command stylua --check . lua-lint: name: Lua Lint Check @@ -50,7 +50,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v31 - - run: nix develop --command selene --display-style quiet . + - run: nix develop .#ci --command selene --display-style quiet . lua-typecheck: name: Lua Type Check @@ -74,7 +74,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v31 - - run: nix develop --command vimdoc-language-server check doc/ + - run: nix develop .#ci --command vimdoc-language-server check doc/ markdown-format: name: Markdown Format Check @@ -84,4 +84,4 @@ jobs: steps: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v31 - - run: nix develop --command prettier --check . + - run: nix develop .#ci --command prettier --check . diff --git a/flake.nix b/flake.nix index 0c7245c..a8de7e7 100644 --- a/flake.nix +++ b/flake.nix @@ -40,21 +40,24 @@ chmod +x "$tmpdir/nvim" PATH="$tmpdir:$PATH" exec ${luaEnv}/bin/busted "$@" ''; + vimdoc-ls = vimdoc-language-server.packages.${pkgs.system}.default; + vimdoc-ls-ci = pkgs.writeShellScriptBin "vimdoc-language-server" '' + exec ${vimdoc-ls}/bin/vimdoc-language-server --runtime-tags "$@" + ''; commonPackages = [ busted-with-grammar pkgs.prettier pkgs.stylua pkgs.selene pkgs.lua-language-server - vimdoc-language-server.packages.${pkgs.system}.default ]; in { default = pkgs.mkShell { - packages = commonPackages; + packages = commonPackages ++ [ vimdoc-ls ]; }; ci = pkgs.mkShell { - packages = commonPackages ++ [ pkgs.neovim ]; + packages = commonPackages ++ [ pkgs.neovim vimdoc-ls-ci ]; }; } ); diff --git a/lua/diffs/git.lua b/lua/diffs/git.lua index 1aa6328..9e666ff 100644 --- a/lua/diffs/git.lua +++ b/lua/diffs/git.lua @@ -2,7 +2,7 @@ local M = {} local repo_root_cache = {} ----@param filepath string +---@param filepath? string ---@return string? function M.get_repo_root(filepath) local dir = vim.fn.fnamemodify(filepath, ':h') From d79be0040b242c9a5eed98b2f9089b8873b98ed5 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Mar 2026 17:03:00 -0400 Subject: [PATCH 4/8] feat(commands): make `:Greview` auto-detect default branch Problem: `:Greview` required a base ref argument, making it cumbersome for the common case of reviewing against the default branch. Solution: make the base argument optional. When omitted, auto-detect the remote default branch via `git symbolic-ref refs/remotes/origin/HEAD`. Also fix pre-existing stylua formatting issues and update vimdoc/README. --- README.md | 1 + doc/diffs.nvim.txt | 22 +++++++++++++-------- lua/diffs/commands.lua | 44 ++++++++++++++++++++++++++++++------------ 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index cca39c7..b1e96a4 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ highlighting driven by treesitter. [vscode-diff](https://github.com/esmuellert/codediff.nvim) FFI backend for word-level accuracy) - `:Gdiff` unified diff against any revision +- `:Greview` full-repo review diff with qflist/loclist navigation - Inline merge conflict detection, highlighting, and resolution - gitsigns.nvim blame popup highlighting - Email quoting/patch syntax support (`> diff ...`) diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index 1b65539..733e9cc 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -446,10 +446,13 @@ COMMANDS *diffs-commands* :Ghdiff [revision] *:Ghdiff* Like |:Gdiff| but explicitly opens in a horizontal split. -:Greview {base} *:Greview* - Open a unified diff of the entire repository against {base} (a git ref - such as `origin/main`, `HEAD~5`, or a commit SHA). Displays in a - horizontal split. +:Greview [base] *:Greview* + Open a unified diff of the entire repository against [base]. Displays + in a horizontal split. + + When called with no arguments, diffs against the remote default branch + (detected via `git symbolic-ref refs/remotes/origin/HEAD`). If the + remote HEAD ref is not set, run `git remote set-head origin -a` first. Populates the quickfix list with file entries (one per changed file, with `+N`/`-M` stats) and the location list with hunk entries (one per @@ -464,15 +467,18 @@ COMMANDS *diffs-commands* diff replaces its buffer instead of creating another split. Parameters: ~ - {base} (string) Git ref to diff against. Required. + {base} (string, optional) Git ref to diff against. Defaults to + the remote default branch. Examples: >vim + :Greview " diff against remote default branch :Greview origin/main :Greview HEAD~10 :Greview abc1234 < Lua API: >lua + require('diffs.commands').greview() require('diffs.commands').greview('origin/main') require('diffs.commands').greview('origin/main', { vertical = true }) require('diffs.commands').greview('origin/main', { repo_root = '/path/to/repo' }) @@ -600,13 +606,13 @@ Enable vim-fugitive (https://github.com/tpope/vim-fugitive) support: >lua vim.g.diffs = { integrations = { fugitive = true } } < -|:Git| status and commit views receive treesitter syntax, line backgrounds, +`:Git` status and commit views receive treesitter syntax, line backgrounds, and intra-line diffs. |:Gdiff| opens a unified diff against any revision (see |diffs-commands|). Fugitive status keymaps: ~ -When inside a |:Git| status buffer, diffs.nvim provides keymaps to open +When inside a `:Git` status buffer, diffs.nvim provides keymaps to open unified diffs for files or entire sections. Keymaps: ~ @@ -1032,7 +1038,7 @@ Conflict highlights: ~ DiffsConflictActions Dimmed foreground (no bold) for the codelens-style action line shown when `show_actions` is true. -Diff mode window highlights: ~ These are used for |winhighlight| remapping in +Diff mode window highlights: ~ These are used for 'winhighlight' remapping in `&diff` windows. *DiffsDiffAdd* diff --git a/lua/diffs/commands.lua b/lua/diffs/commands.lua index 31840bc..6adaad1 100644 --- a/lua/diffs/commands.lua +++ b/lua/diffs/commands.lua @@ -366,15 +366,30 @@ end ---@field vertical? boolean ---@field repo_root? string ----@param base string +---@return string? +local function default_branch() + local ref = vim.fn.system({ 'git', 'symbolic-ref', 'refs/remotes/origin/HEAD' }) + if vim.v.shell_error ~= 0 then + return nil + end + return vim.trim(ref):gsub('^refs/remotes/', '') +end + +---@param base? string ---@param opts? diffs.GreviewOpts ---@return integer? function M.greview(base, opts) opts = opts or {} if not base or base == '' then - vim.notify('[diffs.nvim]: greview requires a base ref', vim.log.levels.ERROR) - return nil + base = default_branch() + if not base then + vim.notify( + '[diffs.nvim]: cannot detect default branch (try: git remote set-head origin -a)', + vim.log.levels.ERROR + ) + return nil + end end local repo_root = opts.repo_root @@ -478,13 +493,11 @@ function M.greview(base, opts) local padded = file .. string.rep(' ', max_fname - #file) local parts = { padded } if max_add > 0 then - parts[#parts + 1] = a > 0 - and string.format('%' .. max_add .. 's', '+' .. a) + parts[#parts + 1] = a > 0 and string.format('%' .. max_add .. 's', '+' .. a) or string.rep(' ', max_add) end if max_del > 0 then - parts[#parts + 1] = d > 0 - and string.format('%' .. max_del .. 's', '-' .. d) + parts[#parts + 1] = d > 0 and string.format('%' .. max_del .. 's', '-' .. d) or string.rep(' ', max_del) end item.text = table.concat(parts, ' ') @@ -497,7 +510,10 @@ function M.greview(base, opts) for _, item in ipairs(loc_items) do item.text = item.text .. string.rep(' ', max_loc_fname - #item.text) - .. ' (hunk ' .. item._hunk .. ') ' .. item._header + .. ' (hunk ' + .. item._hunk + .. ') ' + .. item._header item._hunk = nil item._header = nil end @@ -652,11 +668,15 @@ function M.setup() vim.api.nvim_create_user_command('Greview', function(opts) M.greview(opts.args ~= '' and opts.args or nil) end, { - nargs = 1, + nargs = '?', complete = function(arglead) local refs = vim.fn.systemlist({ - 'git', 'for-each-ref', '--format=%(refname:short)', - 'refs/heads/', 'refs/remotes/', 'refs/tags/', + 'git', + 'for-each-ref', + '--format=%(refname:short)', + 'refs/heads/', + 'refs/remotes/', + 'refs/tags/', }) if vim.v.shell_error ~= 0 then return {} @@ -672,7 +692,7 @@ function M.setup() end return matches end, - desc = 'Show unified diff against a git ref with qflist/loclist', + desc = 'Review diff against the default branch or a given git ref', }) end From cf1f92424c808c76210ea0252758ed0ed8da8af2 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Mar 2026 17:07:06 -0400 Subject: [PATCH 5/8] fix(commands): loclist on wrong window, trailing spaces in qflist Problem: `setloclist` was called before the diffs split, so the loclist landed on the source window instead of the review buffer window. Qflist entries also had trailing whitespace when a file had no adds or no deletes. Solution: move `setloclist` after the window switch. Trim trailing whitespace from qflist text entries. --- lua/diffs/commands.lua | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lua/diffs/commands.lua b/lua/diffs/commands.lua index 6adaad1..bc54daa 100644 --- a/lua/diffs/commands.lua +++ b/lua/diffs/commands.lua @@ -500,7 +500,7 @@ function M.greview(base, opts) parts[#parts + 1] = d > 0 and string.format('%' .. max_del .. 's', '-' .. d) or string.rep(' ', max_del) end - item.text = table.concat(parts, ' ') + item.text = table.concat(parts, ' '):gsub('%s+$', '') end local max_loc_fname = 0 @@ -522,10 +522,6 @@ function M.greview(base, opts) title = 'review: ' .. base, items = qf_items, }) - vim.fn.setloclist(0, {}, ' ', { - title = 'review hunks: ' .. base, - items = loc_items, - }) local existing_win = M.find_diffs_window() if existing_win then @@ -536,6 +532,11 @@ function M.greview(base, opts) vim.api.nvim_win_set_buf(0, diff_buf) end + vim.fn.setloclist(0, {}, ' ', { + title = 'review hunks: ' .. base, + items = loc_items, + }) + M.setup_diff_buf(diff_buf) dbg('opened review buffer %d against %s', diff_buf, base) From 3fafe98e53b7429b8abd92eb613179b065391cd5 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Mar 2026 17:11:36 -0400 Subject: [PATCH 6/8] fix(commands): drop @@ header from loclist entries Problem: loclist entries included the `@@` hunk header which has variable-width line ranges, making alignment impossible. Solution: show only aligned filename + `(hunk N)`, matching the qflist format style. --- lua/diffs/commands.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/diffs/commands.lua b/lua/diffs/commands.lua index bc54daa..a41ea7a 100644 --- a/lua/diffs/commands.lua +++ b/lua/diffs/commands.lua @@ -512,8 +512,7 @@ function M.greview(base, opts) .. string.rep(' ', max_loc_fname - #item.text) .. ' (hunk ' .. item._hunk - .. ') ' - .. item._header + .. ')' item._hunk = nil item._header = nil end From 3c47896a658b0e7e79c7f8494b1bf2b0793c9218 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Mar 2026 17:39:27 -0400 Subject: [PATCH 7/8] feat(commands): per-list quickfixtextfunc with lnum Problem: quickfix and loclist from `:Greview` had no line numbers and inconsistent formats. The custom `quickfixtextfunc` was set globally, affecting unrelated quickfix lists. Solution: define `_diffs_qftf` and set it per-list via the `quickfixtextfunc` property on `setqflist`/`setloclist`. Both lists now show aligned `lnum text`, giving consistent structure. Restore the `@@` hunk header in loclist entries for context. --- lua/diffs/commands.lua | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/lua/diffs/commands.lua b/lua/diffs/commands.lua index a41ea7a..8e745a9 100644 --- a/lua/diffs/commands.lua +++ b/lua/diffs/commands.lua @@ -362,6 +362,31 @@ function M.gdiff_section(repo_root, opts) end) end +-- selene: allow(global_usage) +function _G._diffs_qftf(info) + local items = info.quickfix == 1 and vim.fn.getqflist({ id = info.id, items = 0 }).items + or vim.fn.getloclist(0, { id = info.id, items = 0 }).items + local max_lnum = 0 + for i = info.start_idx, info.end_idx do + local e = items[i] + if e.lnum > 0 then + max_lnum = math.max(max_lnum, #tostring(e.lnum)) + end + end + local lnum_fmt = '%' .. math.max(max_lnum, 1) .. 'd' + local lines = {} + for i = info.start_idx, info.end_idx do + local e = items[i] + local text = e.text or '' + if max_lnum > 0 and e.lnum > 0 then + table.insert(lines, ('%s %s'):format(lnum_fmt:format(e.lnum), text)) + else + table.insert(lines, text) + end + end + return lines +end + ---@class diffs.GreviewOpts ---@field vertical? boolean ---@field repo_root? string @@ -512,7 +537,8 @@ function M.greview(base, opts) .. string.rep(' ', max_loc_fname - #item.text) .. ' (hunk ' .. item._hunk - .. ')' + .. ') ' + .. item._header item._hunk = nil item._header = nil end @@ -520,6 +546,7 @@ function M.greview(base, opts) vim.fn.setqflist({}, ' ', { title = 'review: ' .. base, items = qf_items, + quickfixtextfunc = 'v:lua._diffs_qftf', }) local existing_win = M.find_diffs_window() @@ -534,6 +561,7 @@ function M.greview(base, opts) vim.fn.setloclist(0, {}, ' ', { title = 'review hunks: ' .. base, items = loc_items, + quickfixtextfunc = 'v:lua._diffs_qftf', }) M.setup_diff_buf(diff_buf) From daa9ae27626b098b49dba37e53145cd74b35bcbd Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 23 Mar 2026 17:47:28 -0400 Subject: [PATCH 8/8] fix(build): remove nonexistent --runtime-tags wrapper --- flake.nix | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index a8de7e7..51f5038 100644 --- a/flake.nix +++ b/flake.nix @@ -41,23 +41,21 @@ PATH="$tmpdir:$PATH" exec ${luaEnv}/bin/busted "$@" ''; vimdoc-ls = vimdoc-language-server.packages.${pkgs.system}.default; - vimdoc-ls-ci = pkgs.writeShellScriptBin "vimdoc-language-server" '' - exec ${vimdoc-ls}/bin/vimdoc-language-server --runtime-tags "$@" - ''; commonPackages = [ busted-with-grammar pkgs.prettier pkgs.stylua pkgs.selene pkgs.lua-language-server + vimdoc-ls ]; in { default = pkgs.mkShell { - packages = commonPackages ++ [ vimdoc-ls ]; + packages = commonPackages; }; ci = pkgs.mkShell { - packages = commonPackages ++ [ pkgs.neovim vimdoc-ls-ci ]; + packages = commonPackages ++ [ pkgs.neovim ]; }; } );