diffs.nvim/spec/email_quote_spec.lua
Barrett Ruth a6433da5f4
feat(parser,highlight): support email-quoted diffs (#141)
Problem: email-quoted diffs (`> diff --git ...`, `> @@ ...`) from
git-send-email workflows produce 0 hunks because the parser matches
patterns against raw lines containing `> ` quote prefixes.

Solution: strip quote prefix before pattern matching in the parser,
store `quote_width` on each hunk, offset all extmark column positions
by `qw` in `highlight.lua`, and expand `pw > 1` guards to
`qw > 0 or pw > 1` for DiffsClear suppression. Clamp body prefix
DiffsClear `end_col` to actual buffer line length for bare `>` lines
where `nvim_buf_set_extmark` would reject out-of-range columns.
2026-03-05 10:28:57 -05:00

472 lines
13 KiB
Lua

require('spec.helpers')
local parser = require('diffs.parser')
local highlight = require('diffs.highlight')
describe('email-quoted diffs', function()
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)
return bufnr
end
local function delete_buffer(bufnr)
if vim.api.nvim_buf_is_valid(bufnr) then
vim.api.nvim_buf_delete(bufnr, { force = true })
end
end
describe('parser', function()
it('parses a fully email-quoted unified diff', function()
local bufnr = create_buffer({
'> diff --git a/foo.py b/foo.py',
'> index abc1234..def5678 100644',
'> --- a/foo.py',
'> +++ b/foo.py',
'> @@ -0,0 +1,3 @@',
'> +from typing import Annotated, final',
'> +',
'> +class Foo:',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal('foo.py', hunks[1].filename)
assert.are.equal(3, #hunks[1].lines)
assert.are.equal('+from typing import Annotated, final', hunks[1].lines[1])
assert.are.equal(2, hunks[1].quote_width)
delete_buffer(bufnr)
end)
it('parses a quoted diff embedded in an email reply', function()
local bufnr = create_buffer({
'Looks good, one nit:',
'',
'> diff --git a/foo.py b/foo.py',
'> @@ -0,0 +1,3 @@',
'> +from typing import Annotated, final',
'> +',
'> +class Foo:',
'',
'Maybe rename Foo to Bar?',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal('foo.py', hunks[1].filename)
assert.are.equal(3, #hunks[1].lines)
assert.are.equal(2, hunks[1].quote_width)
delete_buffer(bufnr)
end)
it('sets quote_width = 0 on normal (unquoted) diffs', function()
local bufnr = create_buffer({
'diff --git a/bar.lua b/bar.lua',
'@@ -1,2 +1,2 @@',
'-old_line',
'+new_line',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal(0, hunks[1].quote_width)
delete_buffer(bufnr)
end)
it('treats bare > lines as empty quoted lines', function()
local bufnr = create_buffer({
'> diff --git a/foo.py b/foo.py',
'> @@ -1,3 +1,3 @@',
'> -old',
'>',
'> +new',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal(3, #hunks[1].lines)
assert.are.equal('-old', hunks[1].lines[1])
assert.are.equal(' ', hunks[1].lines[2])
assert.are.equal('+new', hunks[1].lines[3])
delete_buffer(bufnr)
end)
it('handles deeply nested quotes', function()
local bufnr = create_buffer({
'>> diff --git a/foo.py b/foo.py',
'>> @@ -0,0 +1,2 @@',
'>> +line1',
'>> +line2',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal(3, hunks[1].quote_width)
assert.are.equal('+line1', hunks[1].lines[1])
delete_buffer(bufnr)
end)
it('adjusts header_context_col for quote width', function()
local bufnr = create_buffer({
'> diff --git a/foo.py b/foo.py',
'> @@ -1,2 +1,2 @@ def hello():',
'> -old',
'> +new',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal('def hello():', hunks[1].header_context)
assert.are.equal(#'@@ -1,2 +1,2 @@ ' + 2, hunks[1].header_context_col)
delete_buffer(bufnr)
end)
it('does not false-positive on prose containing > diff', function()
local bufnr = create_buffer({
'> diff between approaches is small',
'> I think we should go with option A',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(0, #hunks)
delete_buffer(bufnr)
end)
it('stores header lines stripped of quote prefix', function()
local bufnr = create_buffer({
'> diff --git a/foo.lua b/foo.lua',
'> index abc1234..def5678 100644',
'> --- a/foo.lua',
'> +++ b/foo.lua',
'> @@ -1,1 +1,1 @@',
'> -old',
'> +new',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.is_not_nil(hunks[1].header_lines)
for _, hline in ipairs(hunks[1].header_lines) do
assert.is_nil(hline:match('^> '))
end
delete_buffer(bufnr)
end)
end)
describe('highlight', function()
local ns
before_each(function()
ns = vim.api.nvim_create_namespace('diffs_email_test')
vim.api.nvim_set_hl(0, 'DiffsClear', { fg = 0xc0c0c0, bg = 0x1e1e1e })
vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = 0x1a3a1a })
vim.api.nvim_set_hl(0, 'DiffsDelete', { bg = 0x3a1a1a })
vim.api.nvim_set_hl(0, 'DiffsConflictMarker', { fg = 0x808080, bold = true })
end)
local function get_extmarks(bufnr)
return vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
end
local function default_opts(overrides)
local opts = {
hide_prefix = false,
highlights = {
background = true,
gutter = false,
context = { enabled = false, lines = 0 },
treesitter = {
enabled = true,
max_lines = 500,
},
vim = {
enabled = false,
max_lines = 200,
},
intra = {
enabled = false,
algorithm = 'default',
max_lines = 500,
},
priorities = {
clear = 198,
syntax = 199,
line_bg = 200,
char_bg = 201,
},
},
}
if overrides then
if overrides.highlights then
opts.highlights = vim.tbl_deep_extend('force', opts.highlights, overrides.highlights)
end
for k, v in pairs(overrides) do
if k ~= 'highlights' then
opts[k] = v
end
end
end
return opts
end
it('applies DiffsClear on email-quoted header lines covering full buffer width', function()
local buf_lines = {
'> diff --git a/foo.lua b/foo.lua',
'> index abc1234..def5678 100644',
'> --- a/foo.lua',
'> +++ b/foo.lua',
'> @@ -1,1 +1,1 @@',
'> -old',
'> +new',
}
local bufnr = create_buffer(buf_lines)
local hunk = {
filename = 'foo.lua',
lang = 'lua',
ft = 'lua',
start_line = 5,
lines = { '-old', '+new' },
prefix_width = 1,
quote_width = 2,
header_start_line = 1,
header_lines = {
'diff --git a/foo.lua b/foo.lua',
'index abc1234..def5678 100644',
'--- a/foo.lua',
'+++ b/foo.lua',
},
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local header_clears = {}
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d and d.hl_group == 'DiffsClear' and mark[2] < 4 then
table.insert(header_clears, { row = mark[2], col = mark[3], end_col = d.end_col })
end
end
assert.is_true(#header_clears > 0)
for _, c in ipairs(header_clears) do
assert.are.equal(0, c.col)
local buf_line_len = #buf_lines[c.row + 1]
assert.are.equal(buf_line_len, c.end_col)
end
delete_buffer(bufnr)
end)
it('applies body prefix DiffsClear covering [0, pw+qw)', function()
local bufnr = create_buffer({
'> @@ -1,1 +1,1 @@',
'> -old',
'> +new',
})
local hunk = {
filename = 'foo.lua',
lang = 'lua',
ft = 'lua',
start_line = 1,
lines = { '-old', '+new' },
prefix_width = 1,
quote_width = 2,
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local prefix_clears = {}
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d and d.hl_group == 'DiffsClear' and d.end_col == 3 and mark[3] == 0 then
table.insert(prefix_clears, { row = mark[2] })
end
end
assert.are.equal(2, #prefix_clears)
delete_buffer(bufnr)
end)
it('clamps body prefix DiffsClear on bare > lines (1-byte buffer line)', function()
local bufnr = create_buffer({
'> @@ -1,3 +1,3 @@',
'> -old',
'>',
'> +new',
})
local hunk = {
filename = 'foo.lua',
ft = 'lua',
lang = 'lua',
start_line = 1,
lines = { '-old', ' ', '+new' },
prefix_width = 1,
quote_width = 2,
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local bare_line_row = 2
local bare_clears = {}
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d and d.hl_group == 'DiffsClear' and mark[2] == bare_line_row and mark[3] == 0 then
table.insert(bare_clears, { end_col = d.end_col })
end
end
assert.are.equal(1, #bare_clears)
assert.are.equal(1, bare_clears[1].end_col)
delete_buffer(bufnr)
end)
it('applies per-char @diff.plus/@diff.minus at ci + qw', function()
local bufnr = create_buffer({
'> @@ -1,1 +1,1 @@',
'> -old',
'> +new',
})
local hunk = {
filename = 'foo.lua',
lang = 'lua',
ft = 'lua',
start_line = 1,
lines = { '-old', '+new' },
prefix_width = 1,
quote_width = 2,
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local diff_marks = {}
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d and (d.hl_group == '@diff.plus' or d.hl_group == '@diff.minus') then
table.insert(diff_marks, { row = mark[2], col = mark[3], end_col = d.end_col, hl = d.hl_group })
end
end
assert.is_true(#diff_marks >= 2)
for _, dm in ipairs(diff_marks) do
assert.are.equal(2, dm.col)
assert.are.equal(3, dm.end_col)
end
delete_buffer(bufnr)
end)
it('offsets treesitter extmarks by pw + qw', function()
local bufnr = create_buffer({
'> @@ -1,1 +1,2 @@',
'> local x = 1',
'> +local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
ft = 'lua',
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
prefix_width = 1,
quote_width = 2,
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local ts_marks = {}
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d and d.hl_group and d.hl_group:match('^@.*%.lua$') then
table.insert(ts_marks, { row = mark[2], col = mark[3] })
end
end
assert.is_true(#ts_marks > 0)
for _, tm in ipairs(ts_marks) do
assert.is_true(tm.col >= 3)
end
delete_buffer(bufnr)
end)
it('offsets intra-line char span extmarks by qw', function()
local bufnr = create_buffer({
'> @@ -1,1 +1,1 @@',
'> -hello world',
'> +hello earth',
})
local hunk = {
filename = 'test.txt',
ft = nil,
lang = nil,
start_line = 1,
lines = { '-hello world', '+hello earth' },
prefix_width = 1,
quote_width = 2,
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({ highlights = { intra = { enabled = true, algorithm = 'default', max_lines = 500 } } })
)
local extmarks = get_extmarks(bufnr)
local char_marks = {}
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d and (d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText') then
table.insert(char_marks, { row = mark[2], col = mark[3], end_col = d.end_col })
end
end
if #char_marks > 0 then
for _, cm in ipairs(char_marks) do
assert.is_true(cm.col >= 2)
end
end
delete_buffer(bufnr)
end)
it('does not produce duplicate extmarks with syntax_only + qw', function()
local bufnr = create_buffer({
'> @@ -1,1 +1,2 @@',
'> local x = 1',
'> +local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
ft = 'lua',
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
prefix_width = 1,
quote_width = 2,
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ syntax_only = true }))
local extmarks = get_extmarks(bufnr)
local line_bg_count = 0
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d and d.line_hl_group == 'DiffsAdd' then
line_bg_count = line_bg_count + 1
end
end
assert.are.equal(1, line_bg_count)
delete_buffer(bufnr)
end)
end)
end)