diff --git a/README.md b/README.md index a895fed..30ee301 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ Enhance vim-fugitive and Neovim's built-in diff mode with language-aware syntax highlighting. -![diffs.nvim preview](https://github.com/user-attachments/assets/fc849310-09c8-4282-8a92-a2edaf8fe2b4) +![diffs.nvim preview](https://github.com/user-attachments/assets/d3d64c96-b824-4fcb-af7f-4aef3f7f498a) ## Features - Treesitter syntax highlighting in `:Git` diffs and commit views - `:Gdiffsplit` / `:Gvdiffsplit` syntax through diff backgrounds -- Background-only diff colors for any `&diff` buffer +- Background-only diff colors for any `&diff` buffer (`:diffthis`, `vimdiff`) - Vim syntax fallback for languages without a treesitter parser - Hunk header context highlighting (`@@ ... @@ function foo()`) - Configurable debouncing, max lines, and diff prefix concealment @@ -19,7 +19,8 @@ highlighting. ## Requirements - Neovim 0.9.0+ -- [vim-fugitive](https://github.com/tpope/vim-fugitive) +- [vim-fugitive](https://github.com/tpope/vim-fugitive) (optional, for unified + diff syntax highlighting) ## Installation @@ -39,6 +40,26 @@ Using [lazy.nvim](https://github.com/folke/lazy.nvim): :help diffs.nvim ``` +## Highlight Groups + +diffs.nvim defines the following highlight groups. All use `default = true`, so +colorschemes can override them. + +| Group | Purpose | +| ----------------- | -------------------------------------------------- | +| `DiffsAdd` | Background for `+` lines in fugitive unified diffs | +| `DiffsDelete` | Background for `-` lines in fugitive unified diffs | +| `DiffsAddNr` | Line number highlight for `+` lines | +| `DiffsDeleteNr` | Line number highlight for `-` lines | +| `DiffsDiffAdd` | Background-only `DiffAdd` for `&diff` windows | +| `DiffsDiffDelete` | Background-only `DiffDelete` for `&diff` windows | +| `DiffsDiffChange` | Background-only `DiffChange` for `&diff` windows | +| `DiffsDiffText` | Background-only `DiffText` for `&diff` windows | + +By default, these are computed from your colorscheme's `DiffAdd`, `DiffDelete`, +`DiffChange`, `DiffText`, and `Normal` groups. To customize, define them in your +colorscheme before diffs.nvim loads, or link them to existing groups. + ## Known Limitations - Syntax "flashing": diffs.nvim hooks into the `FileType fugitive` event diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index 3cb0a0c..9535cb0 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -23,9 +23,13 @@ Features: ~ REQUIREMENTS *diffs-requirements* - Neovim 0.9.0+ -- vim-fugitive (https://github.com/tpope/vim-fugitive) for unified diff views +- vim-fugitive (https://github.com/tpope/vim-fugitive) (optional, for unified + diff syntax highlighting in |:Git| and commit views) - Treesitter parsers for languages you want highlighted +Note: The diff mode feature (background-only colors for |:diffthis|, vimdiff, +etc.) works without vim-fugitive. + ============================================================================== SETUP *diffs-setup* @@ -187,6 +191,50 @@ To minimize the flash, use a low debounce value: >lua }) < +============================================================================== +HIGHLIGHT GROUPS *diffs-highlights* + +diffs.nvim defines custom highlight groups. All are set with `default = true`, +so colorschemes can override them by defining them first. + +Fugitive unified diff highlights: ~ + *DiffsAdd* + DiffsAdd Background for `+` lines. Derived by blending + `DiffAdd` background with `Normal` at 40% alpha. + + *DiffsDelete* + DiffsDelete Background for `-` lines. Derived by blending + `DiffDelete` background with `Normal` at 40% alpha. + + *DiffsAddNr* + DiffsAddNr Line number for `+` lines. Foreground from + `diffAdded`, background from `DiffsAdd`. + + *DiffsDeleteNr* + DiffsDeleteNr Line number for `-` lines. Foreground from + `diffRemoved`, background from `DiffsDelete`. + +Diff mode window highlights: ~ +These are background-only variants used for |winhighlight| remapping in +`&diff` windows, allowing treesitter syntax to show through. + + *DiffsDiffAdd* + DiffsDiffAdd Background-only version of `DiffAdd`. + + *DiffsDiffDelete* + DiffsDiffDelete Background-only version of `DiffDelete`. + + *DiffsDiffChange* + DiffsDiffChange Background-only version of `DiffChange`. + + *DiffsDiffText* + DiffsDiffText Background-only version of `DiffText`. + +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' }) +< + ============================================================================== HEALTH CHECK *diffs-health* @@ -194,7 +242,7 @@ Run |:checkhealth| diffs to verify your setup. Checks performed: - Neovim version >= 0.9.0 -- vim-fugitive is installed +- vim-fugitive is installed (optional) ============================================================================== vim:tw=78:ts=8:ft=help:norl: diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua index c925048..b51d338 100644 --- a/lua/diffs/init.lua +++ b/lua/diffs/init.lua @@ -220,16 +220,16 @@ local function compute_highlight_groups() local blended_add = blend_color(add_bg, bg, 0.4) local blended_del = blend_color(del_bg, bg, 0.4) - vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = blended_add }) - vim.api.nvim_set_hl(0, 'DiffsDelete', { bg = blended_del }) - vim.api.nvim_set_hl(0, 'DiffsAddNr', { fg = add_fg, bg = blended_add }) - vim.api.nvim_set_hl(0, 'DiffsDeleteNr', { fg = del_fg, bg = blended_del }) + vim.api.nvim_set_hl(0, 'DiffsAdd', { default = true, bg = blended_add }) + vim.api.nvim_set_hl(0, 'DiffsDelete', { default = true, bg = blended_del }) + vim.api.nvim_set_hl(0, 'DiffsAddNr', { default = true, fg = add_fg, bg = blended_add }) + vim.api.nvim_set_hl(0, 'DiffsDeleteNr', { default = true, fg = del_fg, bg = blended_del }) local diff_change = resolve_hl('DiffChange') local diff_text = resolve_hl('DiffText') vim.api.nvim_set_hl(0, 'DiffsDiffAdd', { bg = diff_add.bg }) - vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { bg = diff_delete.bg }) + vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { fg = diff_delete.fg, bg = diff_delete.bg }) vim.api.nvim_set_hl(0, 'DiffsDiffChange', { bg = diff_change.bg }) vim.api.nvim_set_hl(0, 'DiffsDiffText', { bg = diff_text.bg }) end diff --git a/spec/init_spec.lua b/spec/init_spec.lua index 9d5b461..84fbc99 100644 --- a/spec/init_spec.lua +++ b/spec/init_spec.lua @@ -150,4 +150,159 @@ describe('diffs', function() vim.api.nvim_buf_delete(bufnr, { force = true }) end) end) + + describe('is_fugitive_buffer', function() + it('returns true for fugitive:// URLs', function() + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, 'fugitive:///path/to/repo/.git//abc123:file.lua') + assert.is_true(diffs.is_fugitive_buffer(bufnr)) + vim.api.nvim_buf_delete(bufnr, { force = true }) + end) + + it('returns false for normal paths', function() + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, '/home/user/project/file.lua') + assert.is_false(diffs.is_fugitive_buffer(bufnr)) + vim.api.nvim_buf_delete(bufnr, { force = true }) + end) + + it('returns false for empty buffer names', function() + local bufnr = vim.api.nvim_create_buf(false, true) + assert.is_false(diffs.is_fugitive_buffer(bufnr)) + vim.api.nvim_buf_delete(bufnr, { force = true }) + end) + end) + + describe('diff mode', function() + local function create_diff_window() + vim.cmd('new') + local win = vim.api.nvim_get_current_win() + local buf = vim.api.nvim_get_current_buf() + vim.wo[win].diff = true + return win, buf + end + + local function close_window(win) + if vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_close(win, true) + end + end + + before_each(function() + diffs.setup({ enabled = true }) + end) + + describe('attach_diff', function() + it('applies winhighlight to diff windows', function() + local win, _ = create_diff_window() + diffs.attach_diff() + + local whl = vim.api.nvim_get_option_value('winhighlight', { win = win }) + assert.is_not_nil(whl:match('DiffAdd:DiffsDiffAdd')) + assert.is_not_nil(whl:match('DiffDelete:DiffsDiffDelete')) + + close_window(win) + end) + + it('does nothing when enabled=false', function() + diffs.setup({ enabled = false }) + local win, _ = create_diff_window() + diffs.attach_diff() + + local whl = vim.api.nvim_get_option_value('winhighlight', { win = win }) + assert.are.equal('', whl) + + close_window(win) + end) + + it('is idempotent', function() + local win, _ = create_diff_window() + assert.has_no.errors(function() + diffs.attach_diff() + diffs.attach_diff() + diffs.attach_diff() + end) + + local whl = vim.api.nvim_get_option_value('winhighlight', { win = win }) + assert.is_not_nil(whl:match('DiffAdd:DiffsDiffAdd')) + + close_window(win) + end) + + it('applies to multiple diff windows', function() + local win1, _ = create_diff_window() + local win2, _ = create_diff_window() + diffs.attach_diff() + + local whl1 = vim.api.nvim_get_option_value('winhighlight', { win = win1 }) + local whl2 = vim.api.nvim_get_option_value('winhighlight', { win = win2 }) + assert.is_not_nil(whl1:match('DiffAdd:DiffsDiffAdd')) + assert.is_not_nil(whl2:match('DiffAdd:DiffsDiffAdd')) + + close_window(win1) + close_window(win2) + end) + + it('ignores non-diff windows', function() + vim.cmd('new') + local non_diff_win = vim.api.nvim_get_current_win() + + local diff_win, _ = create_diff_window() + diffs.attach_diff() + + local non_diff_whl = vim.api.nvim_get_option_value('winhighlight', { win = non_diff_win }) + local diff_whl = vim.api.nvim_get_option_value('winhighlight', { win = diff_win }) + + assert.are.equal('', non_diff_whl) + assert.is_not_nil(diff_whl:match('DiffAdd:DiffsDiffAdd')) + + close_window(non_diff_win) + close_window(diff_win) + end) + end) + + describe('detach_diff', function() + it('clears winhighlight from tracked windows', function() + local win, _ = create_diff_window() + diffs.attach_diff() + diffs.detach_diff() + + local whl = vim.api.nvim_get_option_value('winhighlight', { win = win }) + assert.are.equal('', whl) + + close_window(win) + end) + + it('does not error when no windows are tracked', function() + assert.has_no.errors(function() + diffs.detach_diff() + end) + end) + + it('handles already-closed windows gracefully', function() + local win, _ = create_diff_window() + diffs.attach_diff() + close_window(win) + + assert.has_no.errors(function() + diffs.detach_diff() + end) + end) + + it('clears all tracked windows', function() + local win1, _ = create_diff_window() + local win2, _ = create_diff_window() + diffs.attach_diff() + diffs.detach_diff() + + local whl1 = vim.api.nvim_get_option_value('winhighlight', { win = win1 }) + local whl2 = vim.api.nvim_get_option_value('winhighlight', { win = win2 }) + assert.are.equal('', whl1) + assert.are.equal('', whl2) + + close_window(win1) + close_window(win2) + end) + end) + end) end)