feat: gitsigns blame popup highlighting (#157)
## Problem
gitsigns' `:Gitsigns blame_line` popup shows flat
`GitSignsAddPreview`/`GitSignsDeletePreview` line highlights with basic
word-level inline diffs, but no treesitter syntax or diffs.nvim's
character-level intra-line highlighting.
## Solution
Add `lua/diffs/gitsigns.lua` which patches gitsigns' `Popup.create` and
`Popup.update` to intercept blame popups. Parses `Hunk N of M` sections
from the popup buffer, clears gitsigns' own `gitsigns_popup` namespace
on the diff region, and applies `highlight_hunk` with manual
`@diff.plus`/`@diff.minus` prefix extmarks. Uses a separate
`diffs-gitsigns` namespace to avoid colliding with the main decoration
provider.
Enabled via `vim.g.diffs = { gitsigns = true }`. Wired in
`plugin/diffs.lua` with a `User GitAttach` lazy-load retry for when
gitsigns loads after diffs.nvim. Config plumbing adds
`get_highlight_opts()` as a public getter, replacing the
`debug.getupvalue` hack used by the standalone `blame_hl.nvim` plugin.
Closes #155.
This commit is contained in:
parent
c498fd2bac
commit
993fed4a45
7 changed files with 492 additions and 10 deletions
|
|
@ -16,6 +16,7 @@ with language-aware syntax highlighting.
|
||||||
word-level accuracy)
|
word-level accuracy)
|
||||||
- `:Gdiff` unified diff against any revision
|
- `:Gdiff` unified diff against any revision
|
||||||
- Inline merge conflict detection, highlighting, and resolution
|
- Inline merge conflict detection, highlighting, and resolution
|
||||||
|
- gitsigns.nvim blame popup highlighting
|
||||||
- Email quoting/patch syntax support (`> diff ...`)
|
- Email quoting/patch syntax support (`> diff ...`)
|
||||||
- Vim syntax fallback
|
- Vim syntax fallback
|
||||||
- Configurable highlighiting blend & priorities
|
- Configurable highlighiting blend & priorities
|
||||||
|
|
@ -58,14 +59,15 @@ luarocks install diffs.nvim
|
||||||
Do not lazy load `diffs.nvim` with `event`, `lazy`, `ft`, `config`, or `keys` to
|
Do not lazy load `diffs.nvim` with `event`, `lazy`, `ft`, `config`, or `keys` to
|
||||||
control loading - `diffs.nvim` lazy-loads itself.
|
control loading - `diffs.nvim` lazy-loads itself.
|
||||||
|
|
||||||
**Q: Does diffs.nvim support vim-fugitive/Neogit?**
|
**Q: Does diffs.nvim support vim-fugitive/Neogit/gitsigns?**
|
||||||
|
|
||||||
Yes. Enable it in your config:
|
Yes. Enable integrations in your config:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
vim.g.diffs = {
|
vim.g.diffs = {
|
||||||
fugitive = true,
|
fugitive = true,
|
||||||
neogit = true,
|
neogit = true,
|
||||||
|
gitsigns = true,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -119,4 +121,5 @@ See the documentation for more information.
|
||||||
- [`git-conflict.nvim`](https://github.com/akinsho/git-conflict.nvim)
|
- [`git-conflict.nvim`](https://github.com/akinsho/git-conflict.nvim)
|
||||||
- [@phanen](https://github.com/phanen) - diff header highlighting, unknown
|
- [@phanen](https://github.com/phanen) - diff header highlighting, unknown
|
||||||
filetype fix, shebang/modeline detection, treesitter injection support,
|
filetype fix, shebang/modeline detection, treesitter injection support,
|
||||||
decoration provider highlighting architecture
|
decoration provider highlighting architecture, gitsigns blame popup
|
||||||
|
highlighting
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ Features: ~
|
||||||
- Diff header highlighting (`diff --git`, `index`, `---`, `+++`)
|
- Diff header highlighting (`diff --git`, `index`, `---`, `+++`)
|
||||||
- Syntax highlighting in |:Gdiffsplit| / |:Gvdiffsplit| side-by-side diffs
|
- Syntax highlighting in |:Gdiffsplit| / |:Gvdiffsplit| side-by-side diffs
|
||||||
- |:Gdiff| command for unified diff against any git revision
|
- |:Gdiff| command for unified diff against any git revision
|
||||||
|
- gitsigns.nvim blame popup highlighting (see |diffs-gitsigns|)
|
||||||
- Background-only diff colors for any `&diff` buffer (vimdiff, diffthis, etc.)
|
- Background-only diff colors for any `&diff` buffer (vimdiff, diffthis, etc.)
|
||||||
- Vim syntax fallback for languages without a treesitter parser
|
- Vim syntax fallback for languages without a treesitter parser
|
||||||
- Blended diff background colors that preserve syntax visibility
|
- Blended diff background colors that preserve syntax visibility
|
||||||
|
|
@ -35,12 +36,13 @@ CONTENTS *diffs-contents*
|
||||||
8. Conflict Resolution .................................... |diffs-conflict|
|
8. Conflict Resolution .................................... |diffs-conflict|
|
||||||
9. Merge Diff Resolution ..................................... |diffs-merge|
|
9. Merge Diff Resolution ..................................... |diffs-merge|
|
||||||
10. Neogit ................................................... |diffs-neogit|
|
10. Neogit ................................................... |diffs-neogit|
|
||||||
11. API ......................................................... |diffs-api|
|
11. Gitsigns ................................................ |diffs-gitsigns|
|
||||||
12. Implementation ................................... |diffs-implementation|
|
12. API ......................................................... |diffs-api|
|
||||||
13. Known Limitations ................................... |diffs-limitations|
|
13. Implementation ................................... |diffs-implementation|
|
||||||
14. Highlight Groups ..................................... |diffs-highlights|
|
14. Known Limitations ................................... |diffs-limitations|
|
||||||
15. Health Check ............................................. |diffs-health|
|
15. Highlight Groups ..................................... |diffs-highlights|
|
||||||
16. Acknowledgements ............................... |diffs-acknowledgements|
|
16. Health Check ............................................. |diffs-health|
|
||||||
|
17. Acknowledgements ............................... |diffs-acknowledgements|
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
REQUIREMENTS *diffs-requirements*
|
REQUIREMENTS *diffs-requirements*
|
||||||
|
|
@ -78,6 +80,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
||||||
hide_prefix = false,
|
hide_prefix = false,
|
||||||
fugitive = false,
|
fugitive = false,
|
||||||
neogit = false,
|
neogit = false,
|
||||||
|
gitsigns = false,
|
||||||
extra_filetypes = {},
|
extra_filetypes = {},
|
||||||
highlights = {
|
highlights = {
|
||||||
background = true,
|
background = true,
|
||||||
|
|
@ -161,6 +164,16 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
||||||
vim.g.diffs = { neogit = true }
|
vim.g.diffs = { neogit = true }
|
||||||
<
|
<
|
||||||
|
|
||||||
|
{gitsigns} (boolean|table, default: false)
|
||||||
|
Enable gitsigns.nvim blame popup highlighting.
|
||||||
|
Pass `true` or `{}` to enable, `false` to
|
||||||
|
disable. When active, `:Gitsigns blame_line`
|
||||||
|
popups receive treesitter syntax, line
|
||||||
|
backgrounds, and intra-line character diffs.
|
||||||
|
See |diffs-gitsigns|. >lua
|
||||||
|
vim.g.diffs = { gitsigns = true }
|
||||||
|
<
|
||||||
|
|
||||||
{extra_filetypes} (table, default: {})
|
{extra_filetypes} (table, default: {})
|
||||||
Additional filetypes to attach to, beyond the
|
Additional filetypes to attach to, beyond the
|
||||||
built-in `git`, `gitcommit`, and any enabled
|
built-in `git`, `gitcommit`, and any enabled
|
||||||
|
|
@ -650,6 +663,31 @@ line visuals. The overrides are reapplied on `ColorScheme` since Neogit
|
||||||
re-defines its groups then. When `neogit = false`, no highlight overrides
|
re-defines its groups then. When `neogit = false`, no highlight overrides
|
||||||
are applied.
|
are applied.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
GITSIGNS *diffs-gitsigns*
|
||||||
|
|
||||||
|
diffs.nvim can enhance gitsigns.nvim blame popups with syntax highlighting.
|
||||||
|
Enable gitsigns support in your config: >lua
|
||||||
|
vim.g.diffs = { gitsigns = true }
|
||||||
|
<
|
||||||
|
|
||||||
|
When `:Gitsigns blame_line full=true` opens a popup, diffs.nvim intercepts
|
||||||
|
the popup and replaces gitsigns' flat `GitSignsAddPreview`/
|
||||||
|
`GitSignsDeletePreview` highlights with:
|
||||||
|
|
||||||
|
- Treesitter syntax highlighting on the code content
|
||||||
|
- `DiffsAdd`/`DiffsDelete` line backgrounds
|
||||||
|
- Character-level intra-line diffs (`DiffsAddText`/`DiffsDeleteText`)
|
||||||
|
- `@diff.plus`/`@diff.minus` coloring on `+`/`-` prefix characters
|
||||||
|
|
||||||
|
The integration patches `gitsigns.popup.create` and `gitsigns.popup.update`
|
||||||
|
so highlights persist across gitsigns' two-phase render (initial popup, then
|
||||||
|
update with GitHub/PR data). If gitsigns loads after diffs.nvim, a
|
||||||
|
`User GitAttach` autocmd retries the setup automatically.
|
||||||
|
|
||||||
|
Highlights are applied in a separate `diffs-gitsigns` namespace and do not
|
||||||
|
interfere with the main decoration provider used for diff buffers.
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
API *diffs-api*
|
API *diffs-api*
|
||||||
|
|
||||||
|
|
@ -878,7 +916,8 @@ ACKNOWLEDGEMENTS *diffs-acknowledgements*
|
||||||
- codediff.nvim (https://github.com/esmuellert/codediff.nvim)
|
- codediff.nvim (https://github.com/esmuellert/codediff.nvim)
|
||||||
- diffview.nvim (https://github.com/sindrets/diffview.nvim)
|
- diffview.nvim (https://github.com/sindrets/diffview.nvim)
|
||||||
- @phanen (https://github.com/phanen) - diff header highlighting,
|
- @phanen (https://github.com/phanen) - diff header highlighting,
|
||||||
treesitter injection support
|
treesitter injection support, blame_hl.nvim (gitsigns blame popup
|
||||||
|
highlighting inspiration)
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
vim:tw=78:ts=8:ft=help:norl:
|
vim:tw=78:ts=8:ft=help:norl:
|
||||||
|
|
|
||||||
172
lua/diffs/gitsigns.lua
Normal file
172
lua/diffs/gitsigns.lua
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local api = vim.api
|
||||||
|
local fn = vim.fn
|
||||||
|
local dbg = require('diffs.log').dbg
|
||||||
|
|
||||||
|
local ns = api.nvim_create_namespace('diffs-gitsigns')
|
||||||
|
local gs_popup_ns = api.nvim_create_namespace('gitsigns_popup')
|
||||||
|
|
||||||
|
local patched = false
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param src_filename string
|
||||||
|
---@param src_ft string?
|
||||||
|
---@param src_lang string?
|
||||||
|
---@return diffs.Hunk[]
|
||||||
|
function M.parse_blame_hunks(bufnr, src_filename, src_ft, src_lang)
|
||||||
|
local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
local hunks = {}
|
||||||
|
local hunk_lines = {}
|
||||||
|
local hunk_start = nil
|
||||||
|
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line:match('^Hunk %d+ of %d+') then
|
||||||
|
if hunk_start and #hunk_lines > 0 then
|
||||||
|
table.insert(hunks, {
|
||||||
|
filename = src_filename,
|
||||||
|
ft = src_ft,
|
||||||
|
lang = src_lang,
|
||||||
|
start_line = hunk_start,
|
||||||
|
prefix_width = 1,
|
||||||
|
quote_width = 0,
|
||||||
|
lines = hunk_lines,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
hunk_lines = {}
|
||||||
|
hunk_start = i
|
||||||
|
elseif hunk_start then
|
||||||
|
if line:match('^%(guessed:') then
|
||||||
|
hunk_start = i
|
||||||
|
else
|
||||||
|
local prefix = line:sub(1, 1)
|
||||||
|
if prefix == ' ' or prefix == '+' or prefix == '-' then
|
||||||
|
if #hunk_lines == 0 then
|
||||||
|
hunk_start = i - 1
|
||||||
|
end
|
||||||
|
table.insert(hunk_lines, line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if hunk_start and #hunk_lines > 0 then
|
||||||
|
table.insert(hunks, {
|
||||||
|
filename = src_filename,
|
||||||
|
ft = src_ft,
|
||||||
|
lang = src_lang,
|
||||||
|
start_line = hunk_start,
|
||||||
|
prefix_width = 1,
|
||||||
|
quote_width = 0,
|
||||||
|
lines = hunk_lines,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return hunks
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param preview_winid integer
|
||||||
|
---@param preview_bufnr integer
|
||||||
|
local function on_preview(preview_winid, preview_bufnr)
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
if not api.nvim_buf_is_valid(preview_bufnr) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not api.nvim_win_is_valid(preview_winid) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local win = api.nvim_get_current_win()
|
||||||
|
if win == preview_winid then
|
||||||
|
win = fn.win_getid(fn.winnr('#'))
|
||||||
|
end
|
||||||
|
if win == -1 or win == 0 or not api.nvim_win_is_valid(win) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local srcbuf = api.nvim_win_get_buf(win)
|
||||||
|
if not api.nvim_buf_is_loaded(srcbuf) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local ft = vim.bo[srcbuf].filetype
|
||||||
|
local name = api.nvim_buf_get_name(srcbuf)
|
||||||
|
if not name or name == '' then
|
||||||
|
name = ft and ('a.' .. ft) or 'unknown'
|
||||||
|
end
|
||||||
|
local lang = ft and require('diffs.parser').get_lang_from_ft(ft) or nil
|
||||||
|
|
||||||
|
local hunks = M.parse_blame_hunks(preview_bufnr, name, ft, lang)
|
||||||
|
if #hunks == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local diff_start = hunks[1].start_line
|
||||||
|
local last = hunks[#hunks]
|
||||||
|
local diff_end = last.start_line + #last.lines
|
||||||
|
|
||||||
|
api.nvim_buf_clear_namespace(preview_bufnr, gs_popup_ns, diff_start, diff_end)
|
||||||
|
api.nvim_buf_clear_namespace(preview_bufnr, ns, diff_start, diff_end)
|
||||||
|
|
||||||
|
local opts = require('diffs').get_highlight_opts()
|
||||||
|
local highlight = require('diffs.highlight')
|
||||||
|
for _, hunk in ipairs(hunks) do
|
||||||
|
highlight.highlight_hunk(preview_bufnr, ns, hunk, opts)
|
||||||
|
for j, line in ipairs(hunk.lines) do
|
||||||
|
local ch = line:sub(1, 1)
|
||||||
|
if ch == '+' or ch == '-' then
|
||||||
|
pcall(api.nvim_buf_set_extmark, preview_bufnr, ns, hunk.start_line + j - 1, 0, {
|
||||||
|
end_col = 1,
|
||||||
|
hl_group = ch == '+' and '@diff.plus' or '@diff.minus',
|
||||||
|
priority = opts.highlights.priorities.syntax,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
dbg('gitsigns blame: highlighted %d hunks in popup buf %d', #hunks, preview_bufnr)
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
dbg('gitsigns blame error: %s', err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
|
function M.setup()
|
||||||
|
if patched then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local pop_ok, Popup = pcall(require, 'gitsigns.popup')
|
||||||
|
if not pop_ok or not Popup then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
Popup.create = (function(orig)
|
||||||
|
return function(...)
|
||||||
|
local winid, bufnr = orig(...)
|
||||||
|
on_preview(winid, bufnr)
|
||||||
|
return winid, bufnr
|
||||||
|
end
|
||||||
|
end)(Popup.create)
|
||||||
|
|
||||||
|
Popup.update = (function(orig)
|
||||||
|
return function(winid, bufnr, ...)
|
||||||
|
orig(winid, bufnr, ...)
|
||||||
|
on_preview(winid, bufnr)
|
||||||
|
end
|
||||||
|
end)(Popup.update)
|
||||||
|
|
||||||
|
patched = true
|
||||||
|
dbg('gitsigns popup patched')
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
M._test = {
|
||||||
|
parse_blame_hunks = M.parse_blame_hunks,
|
||||||
|
on_preview = on_preview,
|
||||||
|
ns = ns,
|
||||||
|
gs_popup_ns = gs_popup_ns,
|
||||||
|
}
|
||||||
|
|
||||||
|
return M
|
||||||
|
|
@ -38,6 +38,8 @@
|
||||||
|
|
||||||
---@class diffs.NeogitConfig
|
---@class diffs.NeogitConfig
|
||||||
|
|
||||||
|
---@class diffs.GitsignsConfig
|
||||||
|
|
||||||
---@class diffs.ConflictKeymaps
|
---@class diffs.ConflictKeymaps
|
||||||
---@field ours string|false
|
---@field ours string|false
|
||||||
---@field theirs string|false
|
---@field theirs string|false
|
||||||
|
|
@ -62,6 +64,7 @@
|
||||||
---@field highlights diffs.Highlights
|
---@field highlights diffs.Highlights
|
||||||
---@field fugitive diffs.FugitiveConfig|false
|
---@field fugitive diffs.FugitiveConfig|false
|
||||||
---@field neogit diffs.NeogitConfig|false
|
---@field neogit diffs.NeogitConfig|false
|
||||||
|
---@field gitsigns diffs.GitsignsConfig|false
|
||||||
---@field conflict diffs.ConflictConfig
|
---@field conflict diffs.ConflictConfig
|
||||||
|
|
||||||
---@class diffs
|
---@class diffs
|
||||||
|
|
@ -141,6 +144,7 @@ local default_config = {
|
||||||
},
|
},
|
||||||
fugitive = false,
|
fugitive = false,
|
||||||
neogit = false,
|
neogit = false,
|
||||||
|
gitsigns = false,
|
||||||
conflict = {
|
conflict = {
|
||||||
enabled = true,
|
enabled = true,
|
||||||
disable_diagnostics = true,
|
disable_diagnostics = true,
|
||||||
|
|
@ -591,6 +595,10 @@ local function init()
|
||||||
opts.neogit = {}
|
opts.neogit = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if opts.gitsigns == true then
|
||||||
|
opts.gitsigns = {}
|
||||||
|
end
|
||||||
|
|
||||||
vim.validate('debug', opts.debug, function(v)
|
vim.validate('debug', opts.debug, function(v)
|
||||||
return v == nil or type(v) == 'boolean' or type(v) == 'string'
|
return v == nil or type(v) == 'boolean' or type(v) == 'string'
|
||||||
end, 'boolean or string (file path)')
|
end, 'boolean or string (file path)')
|
||||||
|
|
@ -601,6 +609,9 @@ local function init()
|
||||||
vim.validate('neogit', opts.neogit, function(v)
|
vim.validate('neogit', opts.neogit, function(v)
|
||||||
return v == nil or v == false or type(v) == 'table'
|
return v == nil or v == false or type(v) == 'table'
|
||||||
end, 'table or false')
|
end, 'table or false')
|
||||||
|
vim.validate('gitsigns', opts.gitsigns, function(v)
|
||||||
|
return v == nil or v == false or type(v) == 'table'
|
||||||
|
end, 'table or false')
|
||||||
vim.validate('extra_filetypes', opts.extra_filetypes, 'table', true)
|
vim.validate('extra_filetypes', opts.extra_filetypes, 'table', true)
|
||||||
vim.validate('highlights', opts.highlights, 'table', true)
|
vim.validate('highlights', opts.highlights, 'table', true)
|
||||||
|
|
||||||
|
|
@ -990,6 +1001,12 @@ function M.get_conflict_config()
|
||||||
return config.conflict
|
return config.conflict
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return diffs.HunkOpts
|
||||||
|
function M.get_highlight_opts()
|
||||||
|
init()
|
||||||
|
return { hide_prefix = config.hide_prefix, highlights = config.highlights }
|
||||||
|
end
|
||||||
|
|
||||||
local function process_pending_clear(bufnr)
|
local function process_pending_clear(bufnr)
|
||||||
local entry = hunk_cache[bufnr]
|
local entry = hunk_cache[bufnr]
|
||||||
if entry and entry.pending_clear then
|
if entry and entry.pending_clear then
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,8 @@ function M.parse_buffer(bufnr)
|
||||||
return hunks
|
return hunks
|
||||||
end
|
end
|
||||||
|
|
||||||
|
M.get_lang_from_ft = get_lang_from_ft
|
||||||
|
|
||||||
M._test = {
|
M._test = {
|
||||||
ft_lang_cache = ft_lang_cache,
|
ft_lang_cache = ft_lang_cache,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,19 @@ vim.g.loaded_diffs = 1
|
||||||
|
|
||||||
require('diffs.commands').setup()
|
require('diffs.commands').setup()
|
||||||
|
|
||||||
|
local gs_cfg = (vim.g.diffs or {}).gitsigns
|
||||||
|
if gs_cfg == true or type(gs_cfg) == 'table' then
|
||||||
|
if not require('diffs.gitsigns').setup() then
|
||||||
|
vim.api.nvim_create_autocmd('User', {
|
||||||
|
pattern = 'GitAttach',
|
||||||
|
once = true,
|
||||||
|
callback = function()
|
||||||
|
require('diffs.gitsigns').setup()
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd('FileType', {
|
vim.api.nvim_create_autocmd('FileType', {
|
||||||
pattern = require('diffs').compute_filetypes(vim.g.diffs or {}),
|
pattern = require('diffs').compute_filetypes(vim.g.diffs or {}),
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
|
|
|
||||||
236
spec/gitsigns_spec.lua
Normal file
236
spec/gitsigns_spec.lua
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
require('spec.helpers')
|
||||||
|
local gs = require('diffs.gitsigns')
|
||||||
|
|
||||||
|
local function setup_highlight_groups()
|
||||||
|
local normal = vim.api.nvim_get_hl(0, { name = 'Normal' })
|
||||||
|
local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' })
|
||||||
|
local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' })
|
||||||
|
vim.api.nvim_set_hl(0, 'DiffsClear', { fg = normal.fg or 0xc0c0c0 })
|
||||||
|
vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = diff_add.bg or 0x2e4a3a })
|
||||||
|
vim.api.nvim_set_hl(0, 'DiffsDelete', { bg = diff_delete.bg or 0x4a2e3a })
|
||||||
|
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
|
||||||
|
vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
|
||||||
|
end
|
||||||
|
|
||||||
|
local function create_buffer(lines)
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {})
|
||||||
|
return bufnr
|
||||||
|
end
|
||||||
|
|
||||||
|
local function delete_buffer(bufnr)
|
||||||
|
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
|
||||||
|
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe('gitsigns', function()
|
||||||
|
describe('parse_blame_hunks', function()
|
||||||
|
it('parses a single hunk', function()
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'commit abc1234',
|
||||||
|
'Author: Test User',
|
||||||
|
'',
|
||||||
|
'Hunk 1 of 1',
|
||||||
|
' local x = 1',
|
||||||
|
'-local y = 2',
|
||||||
|
'+local y = 3',
|
||||||
|
' local z = 4',
|
||||||
|
})
|
||||||
|
local hunks = gs.parse_blame_hunks(bufnr, 'test.lua', 'lua', 'lua')
|
||||||
|
assert.are.equal(1, #hunks)
|
||||||
|
assert.are.equal('test.lua', hunks[1].filename)
|
||||||
|
assert.are.equal('lua', hunks[1].ft)
|
||||||
|
assert.are.equal('lua', hunks[1].lang)
|
||||||
|
assert.are.equal(1, hunks[1].prefix_width)
|
||||||
|
assert.are.equal(0, hunks[1].quote_width)
|
||||||
|
assert.are.equal(4, #hunks[1].lines)
|
||||||
|
assert.are.equal(4, hunks[1].start_line)
|
||||||
|
assert.are.equal(' local x = 1', hunks[1].lines[1])
|
||||||
|
assert.are.equal('-local y = 2', hunks[1].lines[2])
|
||||||
|
assert.are.equal('+local y = 3', hunks[1].lines[3])
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('parses multiple hunks', function()
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'commit abc1234',
|
||||||
|
'',
|
||||||
|
'Hunk 1 of 2',
|
||||||
|
'-local a = 1',
|
||||||
|
'+local a = 2',
|
||||||
|
'Hunk 2 of 2',
|
||||||
|
' local b = 3',
|
||||||
|
'+local c = 4',
|
||||||
|
})
|
||||||
|
local hunks = gs.parse_blame_hunks(bufnr, 'test.lua', 'lua', 'lua')
|
||||||
|
assert.are.equal(2, #hunks)
|
||||||
|
assert.are.equal(2, #hunks[1].lines)
|
||||||
|
assert.are.equal(2, #hunks[2].lines)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('skips guessed-offset lines', function()
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'commit abc1234',
|
||||||
|
'',
|
||||||
|
'Hunk 1 of 1',
|
||||||
|
'(guessed: hunk offset may be wrong)',
|
||||||
|
' local x = 1',
|
||||||
|
'+local y = 2',
|
||||||
|
})
|
||||||
|
local hunks = gs.parse_blame_hunks(bufnr, 'test.lua', 'lua', 'lua')
|
||||||
|
assert.are.equal(1, #hunks)
|
||||||
|
assert.are.equal(2, #hunks[1].lines)
|
||||||
|
assert.are.equal(' local x = 1', hunks[1].lines[1])
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns empty table when no hunks present', function()
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'commit abc1234',
|
||||||
|
'Author: Test User',
|
||||||
|
'Date: 2024-01-01',
|
||||||
|
})
|
||||||
|
local hunks = gs.parse_blame_hunks(bufnr, 'test.lua', 'lua', 'lua')
|
||||||
|
assert.are.equal(0, #hunks)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('handles hunk with no diff lines after header', function()
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'Hunk 1 of 1',
|
||||||
|
'some non-diff text',
|
||||||
|
})
|
||||||
|
local hunks = gs.parse_blame_hunks(bufnr, 'test.lua', 'lua', 'lua')
|
||||||
|
assert.are.equal(0, #hunks)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('on_preview', function()
|
||||||
|
before_each(function()
|
||||||
|
setup_highlight_groups()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('applies extmarks to popup buffer with diff content', function()
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'commit abc1234',
|
||||||
|
'',
|
||||||
|
'Hunk 1 of 1',
|
||||||
|
' local x = 1',
|
||||||
|
'-local y = 2',
|
||||||
|
'+local y = 3',
|
||||||
|
})
|
||||||
|
|
||||||
|
local winid = vim.api.nvim_open_win(bufnr, false, {
|
||||||
|
relative = 'editor',
|
||||||
|
width = 40,
|
||||||
|
height = 10,
|
||||||
|
row = 0,
|
||||||
|
col = 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
gs._test.on_preview(winid, bufnr)
|
||||||
|
|
||||||
|
local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, gs._test.ns, 0, -1, { details = true })
|
||||||
|
assert.is_true(#extmarks > 0)
|
||||||
|
|
||||||
|
vim.api.nvim_win_close(winid, true)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('clears gitsigns_popup namespace on diff region', function()
|
||||||
|
local bufnr = create_buffer({
|
||||||
|
'commit abc1234',
|
||||||
|
'',
|
||||||
|
'Hunk 1 of 1',
|
||||||
|
' local x = 1',
|
||||||
|
'+local y = 2',
|
||||||
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_extmark(bufnr, gs._test.gs_popup_ns, 3, 0, {
|
||||||
|
end_col = 12,
|
||||||
|
hl_group = 'GitSignsAddPreview',
|
||||||
|
})
|
||||||
|
vim.api.nvim_buf_set_extmark(bufnr, gs._test.gs_popup_ns, 4, 0, {
|
||||||
|
end_col = 12,
|
||||||
|
hl_group = 'GitSignsAddPreview',
|
||||||
|
})
|
||||||
|
|
||||||
|
local winid = vim.api.nvim_open_win(bufnr, false, {
|
||||||
|
relative = 'editor',
|
||||||
|
width = 40,
|
||||||
|
height = 10,
|
||||||
|
row = 0,
|
||||||
|
col = 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
gs._test.on_preview(winid, bufnr)
|
||||||
|
|
||||||
|
local gs_extmarks =
|
||||||
|
vim.api.nvim_buf_get_extmarks(bufnr, gs._test.gs_popup_ns, 0, -1, { details = true })
|
||||||
|
assert.are.equal(0, #gs_extmarks)
|
||||||
|
|
||||||
|
vim.api.nvim_win_close(winid, true)
|
||||||
|
delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('does not error on invalid buffer', function()
|
||||||
|
assert.has_no.errors(function()
|
||||||
|
gs._test.on_preview(0, 99999)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('setup', function()
|
||||||
|
it('returns false when gitsigns.popup is not available', function()
|
||||||
|
local saved = package.loaded['gitsigns.popup']
|
||||||
|
package.loaded['gitsigns.popup'] = nil
|
||||||
|
package.preload['gitsigns.popup'] = nil
|
||||||
|
|
||||||
|
local fresh = loadfile('lua/diffs/gitsigns.lua')()
|
||||||
|
local result = fresh.setup()
|
||||||
|
assert.is_false(result)
|
||||||
|
|
||||||
|
package.loaded['gitsigns.popup'] = saved
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('patches gitsigns.popup when available', function()
|
||||||
|
local create_called = false
|
||||||
|
local update_called = false
|
||||||
|
local mock_popup = {
|
||||||
|
create = function()
|
||||||
|
create_called = true
|
||||||
|
local bufnr = create_buffer({ 'test' })
|
||||||
|
local winid = vim.api.nvim_open_win(bufnr, false, {
|
||||||
|
relative = 'editor',
|
||||||
|
width = 10,
|
||||||
|
height = 1,
|
||||||
|
row = 0,
|
||||||
|
col = 0,
|
||||||
|
})
|
||||||
|
return winid, bufnr
|
||||||
|
end,
|
||||||
|
update = function()
|
||||||
|
update_called = true
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local saved = package.loaded['gitsigns.popup']
|
||||||
|
package.loaded['gitsigns.popup'] = mock_popup
|
||||||
|
|
||||||
|
local fresh = loadfile('lua/diffs/gitsigns.lua')()
|
||||||
|
local result = fresh.setup()
|
||||||
|
assert.is_true(result)
|
||||||
|
|
||||||
|
mock_popup.create()
|
||||||
|
assert.is_true(create_called)
|
||||||
|
|
||||||
|
mock_popup.update(0, 0)
|
||||||
|
assert.is_true(update_called)
|
||||||
|
|
||||||
|
package.loaded['gitsigns.popup'] = saved
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue