fix: pre-release cleanup for v0.2.0 (#102)

## Problem

Three minor issues remain before the v0.2.0 release:

1. Git quotes filenames containing spaces, unicode, or special
characters
   in the fugitive status buffer. `parse_file_line` passed the quotes
   through verbatim, causing file-not-found errors on diff operations.

2. Navigation wrap-around in both conflict and merge modules was silent,
giving no indication when jumping past the last/first item back to the
   beginning/end.

3. `resolved_hunks` and `(resolved)` virtual text in the merge module
persisted across buffer re-reads, showing stale markers for hunks that
   were no longer resolved.

## Solution

1. Add an `unquote()` helper to fugitive.lua that strips surrounding
   quotes and unescapes `\\`, `\"`, `\n`, `\t`, and octal `\NNN`
   sequences. Applied to both return paths in `parse_file_line`.

2. Add `vim.notify` before the wrap-around jump in all four navigation
   functions (`goto_next`/`goto_prev` in conflict.lua and merge.lua).

3. Clear `resolved_hunks[bufnr]` and the merge namespace at the top of
   `setup_keymaps` so each buffer init starts fresh.

Closes #66
This commit is contained in:
Barrett Ruth 2026-02-09 15:08:36 -05:00 committed by GitHub
parent b5d28e9f2b
commit 35067151e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 393 additions and 931 deletions

View file

@ -75,6 +75,12 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
algorithm = 'default',
max_lines = 500,
},
priorities = {
clear = 198,
syntax = 199,
line_bg = 200,
char_bg = 201,
},
overrides = {},
},
fugitive = {
@ -161,6 +167,10 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
Character-level (intra-line) diff highlighting.
See |diffs.IntraConfig| for fields.
{priorities} (table, default: see below)
Extmark priority values.
See |diffs.PrioritiesConfig| for fields.
{overrides} (table, default: {})
Map of highlight group names to highlight
definitions (see |nvim_set_hl()|). Applied
@ -182,6 +192,28 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
direction. Lines are read with early exit —
cost scales with this value, not file size.
*diffs.PrioritiesConfig*
Priorities config fields: ~
{clear} (integer, default: 198)
Priority for `DiffsClear` extmarks that reset
underlying diff foreground colors. Must be
below {syntax}.
{syntax} (integer, default: 199)
Priority for treesitter and vim syntax extmarks.
Must be below {line_bg} so that colorscheme
backgrounds on syntax groups do not obscure
line-level diff backgrounds.
{line_bg} (integer, default: 200)
Priority for `DiffsAdd`/`DiffsDelete` line
background extmarks. Must be below {char_bg}.
{char_bg} (integer, default: 201)
Priority for `DiffsAddText`/`DiffsDeleteText`
character-level background extmarks. Highest
priority so changed characters stand out.
*diffs.TreesitterConfig*
Treesitter config fields: ~
{enabled} (boolean, default: true)
@ -418,6 +450,7 @@ Configuration: ~
disable_diagnostics = true,
show_virtual_text = true,
show_actions = false,
priority = 200,
keymaps = {
ours = 'doo',
theirs = 'dot',
@ -471,6 +504,11 @@ Configuration: ~
`DiffsConflictActions` highlight group.
Only keymaps that are not `false` appear.
{priority} (integer, default: 200)
Extmark priority for conflict region
backgrounds and markers. Adjust if other
plugins use the same priority range.
{keymaps} (table, default: see above)
Buffer-local keymaps for conflict resolution
and navigation. Each value accepts a string
@ -566,12 +604,13 @@ Summary / commit detail views: ~
- Code is parsed with |vim.treesitter.get_string_parser()|
- If no treesitter parser and `vim.enabled`: vim syntax fallback via
scratch buffer and |synID()|
- `Normal` extmarks at priority 198 clear underlying diff foreground
- Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 199
- Syntax highlights are applied as extmarks at priority 200
- `DiffsClear` extmarks at priority 198 clear underlying diff foreground
- Syntax highlights are applied as extmarks at priority 199
- Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 200
- Character-level diff extmarks (`DiffsAddText`/`DiffsDeleteText`) at
priority 201 overlay changed characters with an intense background
- Conceal extmarks hide diff prefixes when `hide_prefix` is enabled
- All priorities are configurable via |diffs.PrioritiesConfig|
4. Re-highlighting occurs on `TextChanged` (debounced) and `Syntax` events
Diff mode views: ~
@ -586,15 +625,10 @@ KNOWN LIMITATIONS *diffs-limitations*
Incomplete Syntax Context ~
*diffs-syntax-context*
Treesitter parses each diff hunk in isolation. To provide surrounding code
context, diffs.nvim reads lines from disk before and after each hunk
(see |diffs.ContextConfig|, enabled by default). This resolves most boundary
issues where incomplete constructs (e.g., a function definition at the edge
of a hunk with no body) would confuse the parser.
Set `highlights.context.enabled = false` to disable context padding. In rare
cases, context padding may not help if the relevant surrounding code is very
far from the hunk boundaries.
Treesitter parses each diff hunk in isolation. Context lines within the hunk
(lines with a ` ` prefix) provide syntactic context for the parser. In rare
cases, hunks that start or end mid-expression may produce imperfect highlights
due to treesitter error recovery.
Syntax Highlighting Flash ~
*diffs-flash*

View file

@ -20,8 +20,6 @@ local attached_buffers = {}
---@type table<integer, boolean>
local diagnostics_suppressed = {}
local PRIORITY_LINE_BG = 200
---@param lines string[]
---@return diffs.ConflictRegion[]
function M.parse(lines)
@ -114,7 +112,7 @@ local function apply_highlights(bufnr, regions, config)
end_row = region.marker_ours + 1,
hl_group = 'DiffsConflictMarker',
hl_eol = true,
priority = PRIORITY_LINE_BG,
priority = config.priority,
})
if config.show_virtual_text then
@ -156,11 +154,11 @@ local function apply_highlights(bufnr, regions, config)
end_row = line + 1,
hl_group = 'DiffsConflictOurs',
hl_eol = true,
priority = PRIORITY_LINE_BG,
priority = config.priority,
})
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
number_hl_group = 'DiffsConflictOursNr',
priority = PRIORITY_LINE_BG,
priority = config.priority,
})
end
@ -169,7 +167,7 @@ local function apply_highlights(bufnr, regions, config)
end_row = region.marker_base + 1,
hl_group = 'DiffsConflictMarker',
hl_eol = true,
priority = PRIORITY_LINE_BG,
priority = config.priority,
})
for line = region.base_start, region.base_end - 1 do
@ -177,11 +175,11 @@ local function apply_highlights(bufnr, regions, config)
end_row = line + 1,
hl_group = 'DiffsConflictBase',
hl_eol = true,
priority = PRIORITY_LINE_BG,
priority = config.priority,
})
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
number_hl_group = 'DiffsConflictBaseNr',
priority = PRIORITY_LINE_BG,
priority = config.priority,
})
end
end
@ -190,7 +188,7 @@ local function apply_highlights(bufnr, regions, config)
end_row = region.marker_sep + 1,
hl_group = 'DiffsConflictMarker',
hl_eol = true,
priority = PRIORITY_LINE_BG,
priority = config.priority,
})
for line = region.theirs_start, region.theirs_end - 1 do
@ -198,11 +196,11 @@ local function apply_highlights(bufnr, regions, config)
end_row = line + 1,
hl_group = 'DiffsConflictTheirs',
hl_eol = true,
priority = PRIORITY_LINE_BG,
priority = config.priority,
})
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
number_hl_group = 'DiffsConflictTheirsNr',
priority = PRIORITY_LINE_BG,
priority = config.priority,
})
end
@ -210,7 +208,7 @@ local function apply_highlights(bufnr, regions, config)
end_row = region.marker_theirs + 1,
hl_group = 'DiffsConflictMarker',
hl_eol = true,
priority = PRIORITY_LINE_BG,
priority = config.priority,
})
if config.show_virtual_text then
@ -364,6 +362,7 @@ function M.goto_next(bufnr)
return
end
end
vim.notify('[diffs.nvim]: wrapped to first conflict', vim.log.levels.INFO)
vim.api.nvim_win_set_cursor(0, { regions[1].marker_ours + 1, 0 })
end
@ -381,6 +380,7 @@ function M.goto_prev(bufnr)
return
end
end
vim.notify('[diffs.nvim]: wrapped to last conflict', vim.log.levels.INFO)
vim.api.nvim_win_set_cursor(0, { regions[#regions].marker_ours + 1, 0 })
end

View file

@ -26,17 +26,62 @@ function M.get_section_at_line(bufnr, lnum)
return nil
end
---@param s string
---@return string
local function unquote(s)
if s:sub(1, 1) ~= '"' then
return s
end
local inner = s:sub(2, -2)
local result = {}
local i = 1
while i <= #inner do
if inner:sub(i, i) == '\\' and i < #inner then
local next_char = inner:sub(i + 1, i + 1)
if next_char == 'n' then
table.insert(result, '\n')
i = i + 2
elseif next_char == 't' then
table.insert(result, '\t')
i = i + 2
elseif next_char == '"' then
table.insert(result, '"')
i = i + 2
elseif next_char == '\\' then
table.insert(result, '\\')
i = i + 2
elseif next_char:match('%d') then
local oct = inner:match('^(%d%d%d)', i + 1)
if oct then
table.insert(result, string.char(tonumber(oct, 8)))
i = i + 4
else
table.insert(result, next_char)
i = i + 2
end
else
table.insert(result, next_char)
i = i + 2
end
else
table.insert(result, inner:sub(i, i))
i = i + 1
end
end
return table.concat(result)
end
---@param line string
---@return string?, string?, string?
local function parse_file_line(line)
local old, new = line:match('^R%d*%s+(.-)%s+->%s+(.+)$')
if old and new then
return vim.trim(new), vim.trim(old), 'R'
return unquote(vim.trim(new)), unquote(vim.trim(old)), 'R'
end
local status, filename = line:match('^([MADRCU?])[MADRCU%s]*%s+(.+)$')
if status and filename then
return vim.trim(filename), nil, status
return unquote(vim.trim(filename)), nil, status
end
return nil, nil, nil

View file

@ -3,38 +3,6 @@ local M = {}
local dbg = require('diffs.log').dbg
local diff = require('diffs.diff')
---@param filepath string
---@param from_line integer
---@param count integer
---@return string[]
local function read_line_range(filepath, from_line, count)
if count <= 0 then
return {}
end
local f = io.open(filepath, 'r')
if not f then
return {}
end
local result = {}
local line_num = 0
for line in f:lines() do
line_num = line_num + 1
if line_num >= from_line then
table.insert(result, line)
if #result >= count then
break
end
end
end
f:close()
return result
end
local PRIORITY_CLEAR = 198
local PRIORITY_SYNTAX = 199
local PRIORITY_LINE_BG = 200
local PRIORITY_CHAR_BG = 201
---@param bufnr integer
---@param ns integer
---@param hunk diffs.Hunk
@ -42,8 +10,9 @@ local PRIORITY_CHAR_BG = 201
---@param text string
---@param lang string
---@param context_lines? string[]
---@param priorities diffs.PrioritiesConfig
---@return integer
local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_lines)
local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_lines, priorities)
local parse_text = text
if context_lines and #context_lines > 0 then
parse_text = text .. '\n' .. table.concat(context_lines, '\n')
@ -77,7 +46,7 @@ local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_l
local buf_sc = col_offset + sc
local buf_ec = col_offset + ec
local priority = lang == 'diff' and (tonumber(metadata.priority) or 100) or PRIORITY_SYNTAX
local priority = lang == 'diff' and (tonumber(metadata.priority) or 100) or priorities.syntax
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
end_row = buf_er,
@ -103,6 +72,7 @@ end
---@param line_map table<integer, integer>
---@param col_offset integer
---@param covered_lines? table<integer, true>
---@param priorities diffs.PrioritiesConfig
---@return integer
local function highlight_treesitter(
bufnr,
@ -111,7 +81,8 @@ local function highlight_treesitter(
lang,
line_map,
col_offset,
covered_lines
covered_lines,
priorities
)
local code = table.concat(code_lines, '\n')
if code == '' then
@ -152,7 +123,7 @@ local function highlight_treesitter(
local buf_ec = ec + col_offset
local priority = tree_lang == 'diff' and (tonumber(metadata.priority) or 100)
or PRIORITY_SYNTAX
or priorities.syntax
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
end_row = buf_er,
@ -219,8 +190,17 @@ end
---@param code_lines string[]
---@param covered_lines? table<integer, true>
---@param leading_offset? integer
---@param priorities diffs.PrioritiesConfig
---@return integer
local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, leading_offset)
local function highlight_vim_syntax(
bufnr,
ns,
hunk,
code_lines,
covered_lines,
leading_offset,
priorities
)
local ft = hunk.ft
if not ft then
return 0
@ -267,7 +247,7 @@ local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines,
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
end_col = span.col_end,
hl_group = span.hl_name,
priority = PRIORITY_SYNTAX,
priority = priorities.syntax,
})
extmark_count = extmark_count + 1
if covered_lines then
@ -284,6 +264,7 @@ end
---@param hunk diffs.Hunk
---@param opts diffs.HunkOpts
function M.highlight_hunk(bufnr, ns, hunk, opts)
local p = opts.highlights.priorities
local use_ts = hunk.lang and opts.highlights.treesitter.enabled
local use_vim = not use_ts and hunk.ft and opts.highlights.vim.enabled
@ -303,21 +284,6 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
---@type table<integer, true>
local covered_lines = {}
local ctx_cfg = opts.highlights.context
local context = (ctx_cfg and ctx_cfg.enabled) and ctx_cfg.lines or 0
local leading = {}
local trailing = {}
if (use_ts or use_vim) and context > 0 and hunk.file_new_start and hunk.repo_root then
local filepath = vim.fs.joinpath(hunk.repo_root, hunk.filename)
local lead_from = math.max(1, hunk.file_new_start - context)
local lead_count = hunk.file_new_start - lead_from
if lead_count > 0 then
leading = read_line_range(filepath, lead_from, lead_count)
end
local trail_from = hunk.file_new_start + (hunk.file_new_count or 0)
trailing = read_line_range(filepath, trail_from, context)
end
local extmark_count = 0
if use_ts then
---@type string[]
@ -329,11 +295,6 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
---@type table<integer, integer>
local old_map = {}
for _, pad_line in ipairs(leading) do
table.insert(new_code, pad_line)
table.insert(old_code, pad_line)
end
for i, line in ipairs(hunk.lines) do
local prefix = line:sub(1, 1)
local stripped = line:sub(2)
@ -352,21 +313,17 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
end
end
for _, pad_line in ipairs(trailing) do
table.insert(new_code, pad_line)
table.insert(old_code, pad_line)
end
extmark_count = highlight_treesitter(bufnr, ns, new_code, hunk.lang, new_map, 1, covered_lines)
extmark_count =
highlight_treesitter(bufnr, ns, new_code, hunk.lang, new_map, 1, covered_lines, p)
extmark_count = extmark_count
+ highlight_treesitter(bufnr, ns, old_code, hunk.lang, old_map, 1, covered_lines)
+ highlight_treesitter(bufnr, ns, old_code, hunk.lang, old_map, 1, covered_lines, p)
if hunk.header_context and hunk.header_context_col then
local header_line = hunk.start_line - 1
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, header_line, hunk.header_context_col, {
end_col = hunk.header_context_col + #hunk.header_context,
hl_group = 'DiffsClear',
priority = PRIORITY_CLEAR,
priority = p.clear,
})
local header_extmarks = highlight_text(
bufnr,
@ -375,7 +332,8 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
hunk.header_context_col,
hunk.header_context,
hunk.lang,
new_code
new_code,
p
)
if header_extmarks > 0 then
dbg('header %s:%d applied %d extmarks', hunk.filename, hunk.start_line, header_extmarks)
@ -385,16 +343,10 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
elseif use_vim then
---@type string[]
local code_lines = {}
for _, pad_line in ipairs(leading) do
table.insert(code_lines, pad_line)
end
for _, line in ipairs(hunk.lines) do
table.insert(code_lines, line:sub(2))
end
for _, pad_line in ipairs(trailing) do
table.insert(code_lines, pad_line)
end
extmark_count = highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, #leading)
extmark_count = highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, 0, p)
end
if
@ -409,7 +361,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
header_map[i] = hunk.header_start_line - 1 + i
end
extmark_count = extmark_count
+ highlight_treesitter(bufnr, ns, hunk.header_lines, 'diff', header_map, 0)
+ highlight_treesitter(bufnr, ns, hunk.header_lines, 'diff', header_map, 0, nil, p)
end
---@type diffs.IntraChanges?
@ -467,7 +419,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, {
end_col = line_len,
hl_group = 'DiffsClear',
priority = PRIORITY_CLEAR,
priority = p.clear,
})
end
@ -476,12 +428,12 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
end_row = buf_line + 1,
hl_group = line_hl,
hl_eol = true,
priority = PRIORITY_LINE_BG,
priority = p.line_bg,
})
if opts.highlights.gutter then
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
number_hl_group = number_hl,
priority = PRIORITY_LINE_BG,
priority = p.line_bg,
})
end
end
@ -501,7 +453,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
local ok, err = pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
end_col = span.col_end,
hl_group = char_hl,
priority = PRIORITY_CHAR_BG,
priority = p.char_bg,
})
if not ok then
dbg('char extmark FAILED: %s', err)

View file

@ -15,6 +15,12 @@
---@field enabled boolean
---@field lines integer
---@class diffs.PrioritiesConfig
---@field clear integer
---@field syntax integer
---@field line_bg integer
---@field char_bg integer
---@class diffs.Highlights
---@field background boolean
---@field gutter boolean
@ -24,6 +30,7 @@
---@field treesitter diffs.TreesitterConfig
---@field vim diffs.VimConfig
---@field intra diffs.IntraConfig
---@field priorities diffs.PrioritiesConfig
---@class diffs.FugitiveConfig
---@field horizontal string|false
@ -43,6 +50,7 @@
---@field show_virtual_text boolean
---@field format_virtual_text? fun(side: string, keymap: string|false): string?
---@field show_actions boolean
---@field priority integer
---@field keymaps diffs.ConflictKeymaps
---@class diffs.Config
@ -121,6 +129,12 @@ local default_config = {
algorithm = 'default',
max_lines = 500,
},
priorities = {
clear = 198,
syntax = 199,
line_bg = 200,
char_bg = 201,
},
},
fugitive = {
horizontal = 'du',
@ -131,6 +145,7 @@ local default_config = {
disable_diagnostics = true,
show_virtual_text = true,
show_actions = false,
priority = 200,
keymaps = {
ours = 'doo',
theirs = 'dot',
@ -328,6 +343,7 @@ local function init()
['highlights.treesitter'] = { opts.highlights.treesitter, 'table', true },
['highlights.vim'] = { opts.highlights.vim, 'table', true },
['highlights.intra'] = { opts.highlights.intra, 'table', true },
['highlights.priorities'] = { opts.highlights.priorities, 'table', true },
})
if opts.highlights.context then
@ -368,6 +384,15 @@ local function init()
['highlights.intra.max_lines'] = { opts.highlights.intra.max_lines, 'number', true },
})
end
if opts.highlights.priorities then
vim.validate({
['highlights.priorities.clear'] = { opts.highlights.priorities.clear, 'number', true },
['highlights.priorities.syntax'] = { opts.highlights.priorities.syntax, 'number', true },
['highlights.priorities.line_bg'] = { opts.highlights.priorities.line_bg, 'number', true },
['highlights.priorities.char_bg'] = { opts.highlights.priorities.char_bg, 'number', true },
})
end
end
if opts.fugitive then
@ -396,6 +421,7 @@ local function init()
['conflict.show_virtual_text'] = { opts.conflict.show_virtual_text, 'boolean', true },
['conflict.format_virtual_text'] = { opts.conflict.format_virtual_text, 'function', true },
['conflict.show_actions'] = { opts.conflict.show_actions, 'boolean', true },
['conflict.priority'] = { opts.conflict.priority, 'number', true },
['conflict.keymaps'] = { opts.conflict.keymaps, 'table', true },
})
@ -457,6 +483,17 @@ local function init()
then
error('diffs: highlights.blend_alpha must be >= 0 and <= 1')
end
if opts.highlights and opts.highlights.priorities then
for _, key in ipairs({ 'clear', 'syntax', 'line_bg', 'char_bg' }) do
local v = opts.highlights.priorities[key]
if v and v < 0 then
error('diffs: highlights.priorities.' .. key .. ' must be >= 0')
end
end
end
if opts.conflict and opts.conflict.priority and opts.conflict.priority < 0 then
error('diffs: conflict.priority must be >= 0')
end
config = vim.tbl_deep_extend('force', default_config, opts)
log.set_enabled(config.debug)

View file

@ -298,6 +298,7 @@ function M.goto_next(bufnr)
end
end
vim.notify('[diffs.nvim]: wrapped to first hunk', vim.log.levels.INFO)
vim.api.nvim_win_set_cursor(0, { candidates[1].start_line + 1, 0 })
end
@ -335,6 +336,7 @@ function M.goto_prev(bufnr)
end
end
vim.notify('[diffs.nvim]: wrapped to last hunk', vim.log.levels.INFO)
vim.api.nvim_win_set_cursor(0, { candidates[#candidates].start_line + 1, 0 })
end
@ -378,6 +380,9 @@ end
---@param bufnr integer
---@param config diffs.ConflictConfig
function M.setup_keymaps(bufnr, config)
resolved_hunks[bufnr] = nil
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
local km = config.keymaps
local maps = {

View file

@ -235,29 +235,6 @@ describe('conflict', function()
helpers.delete_buffer(bufnr)
end)
it('does not apply virtual text when disabled', function()
local bufnr = create_file_buffer({
'<<<<<<< HEAD',
'local x = 1',
'=======',
'local x = 2',
'>>>>>>> feature',
})
conflict.attach(bufnr, default_config({ show_virtual_text = false }))
local extmarks = get_extmarks(bufnr)
local virt_text_count = 0
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].virt_text then
virt_text_count = virt_text_count + 1
end
end
assert.are.equal(0, virt_text_count)
helpers.delete_buffer(bufnr)
end)
it('applies number_hl_group to content lines', function()
local bufnr = create_file_buffer({
'<<<<<<< HEAD',
@ -532,6 +509,33 @@ describe('conflict', function()
helpers.delete_buffer(bufnr)
end)
it('goto_next notifies on wrap-around', function()
local bufnr = create_file_buffer({
'<<<<<<< HEAD',
'a',
'=======',
'b',
'>>>>>>> feat',
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 5, 0 })
local notified = false
local orig_notify = vim.notify
vim.notify = function(msg)
if msg:match('wrapped to first conflict') then
notified = true
end
end
conflict.goto_next(bufnr)
vim.notify = orig_notify
assert.is_true(notified)
helpers.delete_buffer(bufnr)
end)
it('goto_prev jumps to previous conflict', function()
local bufnr = create_file_buffer({
'<<<<<<< HEAD',
@ -576,6 +580,33 @@ describe('conflict', function()
helpers.delete_buffer(bufnr)
end)
it('goto_prev notifies on wrap-around', function()
local bufnr = create_file_buffer({
'<<<<<<< HEAD',
'a',
'=======',
'b',
'>>>>>>> feat',
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 0 })
local notified = false
local orig_notify = vim.notify
vim.notify = function(msg)
if msg:match('wrapped to last conflict') then
notified = true
end
end
conflict.goto_prev(bufnr)
vim.notify = orig_notify
assert.is_true(notified)
helpers.delete_buffer(bufnr)
end)
it('goto_next does nothing with no conflicts', function()
local bufnr = create_file_buffer({ 'normal line' })
vim.api.nvim_set_current_buf(bufnr)

View file

@ -87,28 +87,6 @@ describe('fugitive', function()
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('parses added file', function()
local buf = create_status_buffer({
'Staged (1)',
'A newfile.lua',
})
local filename, section = fugitive.get_file_at_line(buf, 2)
assert.equals('newfile.lua', filename)
assert.equals('staged', section)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('parses deleted file', function()
local buf = create_status_buffer({
'Staged (1)',
'D oldfile.lua',
})
local filename, section = fugitive.get_file_at_line(buf, 2)
assert.equals('oldfile.lua', filename)
assert.equals('staged', section)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('parses renamed file and returns both names', function()
local buf = create_status_buffer({
'Staged (1)',
@ -157,28 +135,6 @@ describe('fugitive', function()
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('handles renamed file in subdirectory', function()
local buf = create_status_buffer({
'Staged (1)',
'R src/old.lua -> src/new.lua',
})
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
assert.equals('src/new.lua', filename)
assert.equals('src/old.lua', old_filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('handles renamed file moved to different directory', function()
local buf = create_status_buffer({
'Staged (1)',
'R old/file.lua -> new/file.lua',
})
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
assert.equals('new/file.lua', filename)
assert.equals('old/file.lua', old_filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('KNOWN LIMITATION: filename containing arrow parsed incorrectly', function()
local buf = create_status_buffer({
'Staged (1)',
@ -190,77 +146,54 @@ describe('fugitive', function()
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('handles double extensions', function()
it('unquotes git-quoted filenames with spaces', function()
local buf = create_status_buffer({
'Unstaged (1)',
'M "path with spaces/file.lua"',
})
local filename = fugitive.get_file_at_line(buf, 2)
assert.equals('path with spaces/file.lua', filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('unquotes escaped quotes in filenames', function()
local buf = create_status_buffer({
'Unstaged (1)',
'M "file\\"name.lua"',
})
local filename = fugitive.get_file_at_line(buf, 2)
assert.equals('file"name.lua', filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('unquotes octal escapes in filenames', function()
local buf = create_status_buffer({
'Unstaged (1)',
'M "\\303\\251le.lua"',
})
local filename = fugitive.get_file_at_line(buf, 2)
assert.equals('\195\169le.lua', filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('passes through unquoted filenames unchanged', function()
local buf = create_status_buffer({
'Unstaged (1)',
'M normal.lua',
})
local filename = fugitive.get_file_at_line(buf, 2)
assert.equals('normal.lua', filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('unquotes renamed files with quotes', function()
local buf = create_status_buffer({
'Staged (1)',
'M test.spec.lua',
'R100 "old name.lua" -> "new name.lua"',
})
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
assert.equals('test.spec.lua', filename)
assert.is_nil(old_filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('handles hyphenated filenames', function()
local buf = create_status_buffer({
'Unstaged (1)',
'M my-component-test.lua',
})
local filename, section = fugitive.get_file_at_line(buf, 2)
assert.equals('my-component-test.lua', filename)
assert.equals('unstaged', section)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('handles underscores and numbers', function()
local buf = create_status_buffer({
'Staged (1)',
'A test_file_123.lua',
})
local filename = fugitive.get_file_at_line(buf, 2)
assert.equals('test_file_123.lua', filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('handles dotfiles', function()
local buf = create_status_buffer({
'Unstaged (1)',
'M .gitignore',
})
local filename = fugitive.get_file_at_line(buf, 2)
assert.equals('.gitignore', filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('handles renamed with complex names', function()
local buf = create_status_buffer({
'Staged (1)',
'R src/old-file.spec.lua -> src/new-file.spec.lua',
})
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
assert.equals('src/new-file.spec.lua', filename)
assert.equals('src/old-file.spec.lua', old_filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('handles deeply nested paths', function()
local buf = create_status_buffer({
'Unstaged (1)',
'M lua/diffs/ui/components/diff-view.lua',
})
local filename = fugitive.get_file_at_line(buf, 2)
assert.equals('lua/diffs/ui/components/diff-view.lua', filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('parses untracked file', function()
local buf = create_status_buffer({
'Untracked (1)',
'? untracked.lua',
})
local filename, section = fugitive.get_file_at_line(buf, 2)
assert.equals('untracked.lua', filename)
assert.equals('untracked', section)
assert.equals('new name.lua', filename)
assert.equals('old name.lua', old_filename)
vim.api.nvim_buf_delete(buf, { force = true })
end)
@ -321,30 +254,6 @@ describe('fugitive', function()
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('detects section header for Unstaged', function()
local buf = create_status_buffer({
'Unstaged (3)',
'M file1.lua',
})
local filename, section, is_header = fugitive.get_file_at_line(buf, 1)
assert.is_nil(filename)
assert.equals('unstaged', section)
assert.is_true(is_header)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('detects section header for Untracked', function()
local buf = create_status_buffer({
'Untracked (1)',
'? newfile.lua',
})
local filename, section, is_header = fugitive.get_file_at_line(buf, 1)
assert.is_nil(filename)
assert.equals('untracked', section)
assert.is_true(is_header)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('returns is_header=false for file lines', function()
local buf = create_status_buffer({
'Staged (1)',
@ -406,22 +315,6 @@ describe('fugitive', function()
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('returns hunk header and offset for - line', function()
local buf = create_status_buffer({
'Unstaged (1)',
'M file.lua',
'@@ -1,3 +1,3 @@',
' local M = {}',
'-local old = false',
' return M',
})
local pos = fugitive.get_hunk_position(buf, 5)
assert.is_not_nil(pos)
assert.equals('@@ -1,3 +1,3 @@', pos.hunk_header)
assert.equals(2, pos.offset)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('returns hunk header and offset for context line', function()
local buf = create_status_buffer({
'Unstaged (1)',

View file

@ -51,6 +51,12 @@ describe('highlight', function()
algorithm = 'default',
max_lines = 500,
},
priorities = {
clear = 198,
syntax = 199,
line_bg = 200,
char_bg = 201,
},
},
}
if overrides then
@ -64,27 +70,6 @@ describe('highlight', function()
return opts
end
it('applies extmarks for lua code', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
assert.is_true(#extmarks > 0)
delete_buffer(bufnr)
end)
it('applies DiffsClear extmarks to clear diff colors', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@ -190,36 +175,6 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('highlights header context when enabled', function()
local bufnr = create_buffer({
'@@ -10,3 +10,4 @@ function hello()',
' local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
header_context = 'function hello()',
header_context_col = 18,
lines = { ' local x = 1', '+local y = 2' },
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local has_header_extmark = false
for _, mark in ipairs(extmarks) do
if mark[2] == 0 then
has_header_extmark = true
break
end
end
assert.is_true(has_header_extmark)
delete_buffer(bufnr)
end)
it('highlights function keyword in header context', function()
local bufnr = create_buffer({
'@@ -5,3 +5,4 @@ function M.setup()',
@ -280,44 +235,6 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('handles empty hunk lines', function()
local bufnr = create_buffer({
'@@ -1,0 +1,0 @@',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = {},
}
assert.has_no.errors(function()
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
end)
delete_buffer(bufnr)
end)
it('handles code that is just whitespace', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' ',
'+ ',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' ', '+ ' },
}
assert.has_no.errors(function()
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
end)
delete_buffer(bufnr)
end)
it('applies overlay extmarks when hide_prefix enabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@ -345,33 +262,6 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('does not apply overlay extmarks when hide_prefix disabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ hide_prefix = false }))
local extmarks = get_extmarks(bufnr)
local overlay_count = 0
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].virt_text_pos == 'overlay' then
overlay_count = overlay_count + 1
end
end
assert.are.equal(0, overlay_count)
delete_buffer(bufnr)
end)
it('applies DiffAdd background to + lines when background enabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@ -438,39 +328,6 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('does not apply background when background disabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({ highlights = { background = false } })
)
local extmarks = get_extmarks(bufnr)
local has_line_hl = false
for _, mark in ipairs(extmarks) do
if mark[4] and (mark[4].hl_group == 'DiffsAdd' or mark[4].hl_group == 'DiffsDelete') then
has_line_hl = true
break
end
end
assert.is_false(has_line_hl)
delete_buffer(bufnr)
end)
it('applies number_hl_group when gutter enabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@ -504,72 +361,6 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('does not apply number_hl_group when gutter disabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({ highlights = { background = true, gutter = false } })
)
local extmarks = get_extmarks(bufnr)
local has_number_hl = false
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].number_hl_group then
has_number_hl = true
break
end
end
assert.is_false(has_number_hl)
delete_buffer(bufnr)
end)
it('skips treesitter highlights when treesitter disabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({ highlights = { treesitter = { enabled = false }, background = true } })
)
local extmarks = get_extmarks(bufnr)
local has_ts_highlight = false
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@') then
has_ts_highlight = true
break
end
end
assert.is_false(has_ts_highlight)
delete_buffer(bufnr)
end)
it('still applies background when treesitter disabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@ -654,40 +445,6 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('skips vim fallback when vim.enabled is false', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
ft = 'abap',
lang = nil,
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({ highlights = { vim = { enabled = false } } })
)
local extmarks = get_extmarks(bufnr)
local has_syntax_hl = false
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'DiffsClear' then
has_syntax_hl = true
break
end
end
assert.is_false(has_syntax_hl)
delete_buffer(bufnr)
end)
it('respects vim.max_lines', function()
local lines = { '@@ -1,100 +1,101 @@' }
local hunk_lines = {}
@ -900,92 +657,6 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('line bg priority > DiffsClear priority', function()
local bufnr = create_buffer({
'@@ -1,2 +1,1 @@',
'-local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { '-local x = 1', '+local y = 2' },
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({ highlights = { background = true } })
)
local extmarks = get_extmarks(bufnr)
local clear_priority = nil
local line_bg_priority = nil
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d and d.hl_group == 'DiffsClear' then
clear_priority = d.priority
end
if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
line_bg_priority = d.priority
end
end
assert.is_not_nil(clear_priority)
assert.is_not_nil(line_bg_priority)
assert.is_true(line_bg_priority > clear_priority)
delete_buffer(bufnr)
end)
it('char-level extmarks have higher priority than line bg', function()
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
local bufnr = create_buffer({
'@@ -1,2 +1,2 @@',
'-local x = 1',
'+local x = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { '-local x = 1', '+local x = 2' },
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({
highlights = {
background = true,
intra = { enabled = true, algorithm = 'default', max_lines = 500 },
},
})
)
local extmarks = get_extmarks(bufnr)
local line_bg_priority = nil
local char_bg_priority = nil
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
line_bg_priority = d.priority
end
if d and (d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText') then
char_bg_priority = d.priority
end
end
assert.is_not_nil(line_bg_priority)
assert.is_not_nil(char_bg_priority)
assert.is_true(char_bg_priority > line_bg_priority)
delete_buffer(bufnr)
end)
it('creates char-level extmarks for changed characters', function()
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
@ -1029,38 +700,6 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('does not create char-level extmarks when intra disabled', function()
local bufnr = create_buffer({
'@@ -1,2 +1,2 @@',
'-local x = 1',
'+local x = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { '-local x = 1', '+local x = 2' },
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({
highlights = { intra = { enabled = false, algorithm = 'default', max_lines = 500 } },
})
)
local extmarks = get_extmarks(bufnr)
for _, mark in ipairs(extmarks) do
local d = mark[4]
assert.is_not_equal('DiffsAddText', d and d.hl_group)
assert.is_not_equal('DiffsDeleteText', d and d.hl_group)
end
delete_buffer(bufnr)
end)
it('does not create char-level extmarks for pure additions', function()
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
@ -1157,142 +796,6 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('context padding produces no extmarks on padding lines', function()
local repo_root = '/tmp/diffs-test-context'
vim.fn.mkdir(repo_root, 'p')
local f = io.open(repo_root .. '/test.lua', 'w')
f:write('local M = {}\n')
f:write('function M.hello()\n')
f:write(' return "hi"\n')
f:write('end\n')
f:write('return M\n')
f:close()
local bufnr = create_buffer({
'@@ -3,1 +3,2 @@',
' return "hi"',
'+"bye"',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' return "hi"', '+"bye"' },
file_old_start = 3,
file_old_count = 1,
file_new_start = 3,
file_new_count = 2,
repo_root = repo_root,
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({ highlights = { context = { enabled = true, lines = 25 } } })
)
local extmarks = get_extmarks(bufnr)
for _, mark in ipairs(extmarks) do
local row = mark[2]
assert.is_true(row >= 1 and row <= 2)
end
delete_buffer(bufnr)
os.remove(repo_root .. '/test.lua')
vim.fn.delete(repo_root, 'rf')
end)
it('context disabled matches behavior without padding', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
file_new_start = 1,
file_new_count = 2,
repo_root = '/nonexistent',
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({ highlights = { context = { enabled = false, lines = 0 } } })
)
local extmarks = get_extmarks(bufnr)
assert.is_true(#extmarks > 0)
delete_buffer(bufnr)
end)
it('gracefully handles missing file for context padding', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
file_new_start = 1,
file_new_count = 2,
repo_root = '/nonexistent/path',
}
assert.has_no.errors(function()
highlight.highlight_hunk(
bufnr,
ns,
hunk,
default_opts({ highlights = { context = { enabled = true, lines = 25 } } })
)
end)
local extmarks = get_extmarks(bufnr)
assert.is_true(#extmarks > 0)
delete_buffer(bufnr)
end)
it('highlights treesitter injections', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
'+vim.cmd([[ echo 1 ]])',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' local x = 1', '+vim.cmd([[ echo 1 ]])' },
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local has_vim_capture = false
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.vim$') then
has_vim_capture = true
break
end
end
assert.is_true(has_vim_capture)
delete_buffer(bufnr)
end)
it('includes captures from both base and injected languages', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@ -1386,6 +889,7 @@ describe('highlight', function()
context = { enabled = false, lines = 0 },
treesitter = { enabled = true, max_lines = 500 },
vim = { enabled = false, max_lines = 200 },
priorities = { clear = 198, syntax = 199, line_bg = 200, char_bg = 201 },
},
}
end
@ -1468,47 +972,6 @@ describe('highlight', function()
assert.are.equal(0, header_extmarks)
delete_buffer(bufnr)
end)
it('does not apply header highlights when treesitter disabled', function()
local bufnr = create_buffer({
'diff --git a/parser.lua b/parser.lua',
'index 3e8afa0..018159c 100644',
'--- a/parser.lua',
'+++ b/parser.lua',
'@@ -1,2 +1,3 @@',
' local M = {}',
'+local x = 1',
})
local hunk = {
filename = 'parser.lua',
lang = 'lua',
start_line = 5,
lines = { ' local M = {}', '+local x = 1' },
header_start_line = 1,
header_lines = {
'diff --git a/parser.lua b/parser.lua',
'index 3e8afa0..018159c 100644',
'--- a/parser.lua',
'+++ b/parser.lua',
},
}
local opts = default_opts()
opts.highlights.treesitter.enabled = false
highlight.highlight_hunk(bufnr, ns, hunk, opts)
local extmarks = get_extmarks(bufnr)
local header_extmarks = 0
for _, mark in ipairs(extmarks) do
if mark[2] < 4 and mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@') then
header_extmarks = header_extmarks + 1
end
end
assert.are.equal(0, header_extmarks)
delete_buffer(bufnr)
end)
end)
describe('extmark priority', function()
@ -1543,40 +1006,11 @@ describe('highlight', function()
context = { enabled = false, lines = 0 },
treesitter = { enabled = true, max_lines = 500 },
vim = { enabled = false, max_lines = 200 },
priorities = { clear = 198, syntax = 199, line_bg = 200, char_bg = 201 },
},
}
end
it('uses priority 199 for code languages', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
'+local y = 2',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' local x = 1', '+local y = 2' },
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local has_priority_199 = false
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.lua$') then
if mark[4].priority == 199 then
has_priority_199 = true
break
end
end
end
assert.is_true(has_priority_199)
delete_buffer(bufnr)
end)
it('uses treesitter priority for diff language', function()
local bufnr = create_buffer({
'diff --git a/test.lua b/test.lua',

View file

@ -509,6 +509,44 @@ describe('merge', function()
helpers.delete_buffer(w_bufnr)
end)
it('goto_next notifies on wrap-around', function()
local working_path = '/tmp/diffs_test_wrap_notify.lua'
local w_bufnr = create_working_buffer({
'<<<<<<< HEAD',
'local x = 1',
'=======',
'local x = 2',
'>>>>>>> feature',
}, working_path)
local d_bufnr = create_diff_buffer({
'diff --git a/file.lua b/file.lua',
'--- a/file.lua',
'+++ b/file.lua',
'@@ -1,1 +1,1 @@',
'-local x = 1',
'+local x = 2',
}, working_path)
vim.api.nvim_set_current_buf(d_bufnr)
vim.api.nvim_win_set_cursor(0, { 6, 0 })
local notified = false
local orig_notify = vim.notify
vim.notify = function(msg)
if msg:match('wrapped to first hunk') then
notified = true
end
end
merge.goto_next(d_bufnr)
vim.notify = orig_notify
assert.is_true(notified)
helpers.delete_buffer(d_bufnr)
helpers.delete_buffer(w_bufnr)
end)
it('goto_prev jumps to previous conflict hunk', function()
local working_path = '/tmp/diffs_test_prev.lua'
local w_bufnr = create_working_buffer({
@ -577,6 +615,44 @@ describe('merge', function()
helpers.delete_buffer(w_bufnr)
end)
it('goto_prev notifies on wrap-around', function()
local working_path = '/tmp/diffs_test_prev_wrap_notify.lua'
local w_bufnr = create_working_buffer({
'<<<<<<< HEAD',
'local x = 1',
'=======',
'local x = 2',
'>>>>>>> feature',
}, working_path)
local d_bufnr = create_diff_buffer({
'diff --git a/file.lua b/file.lua',
'--- a/file.lua',
'+++ b/file.lua',
'@@ -1,1 +1,1 @@',
'-local x = 1',
'+local x = 2',
}, working_path)
vim.api.nvim_set_current_buf(d_bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 0 })
local notified = false
local orig_notify = vim.notify
vim.notify = function(msg)
if msg:match('wrapped to last hunk') then
notified = true
end
end
merge.goto_prev(d_bufnr)
vim.notify = orig_notify
assert.is_true(notified)
helpers.delete_buffer(d_bufnr)
helpers.delete_buffer(w_bufnr)
end)
it('skips resolved hunks', function()
local working_path = '/tmp/diffs_test_skip_resolved.lua'
local w_bufnr = create_working_buffer({
@ -650,8 +726,19 @@ describe('merge', function()
helpers.delete_buffer(d_bufnr)
end)
end)
describe('setup_keymaps', function()
it('clears resolved state on re-init', function()
local working_path = '/tmp/diffs_test_reinit.lua'
local w_bufnr = create_working_buffer({
'<<<<<<< HEAD',
'local x = 1',
'=======',
'local x = 2',
'>>>>>>> feature',
}, working_path)
it('does not add hints when show_virtual_text is false', function()
local d_bufnr = create_diff_buffer({
'diff --git a/file.lua b/file.lua',
'--- a/file.lua',
@ -659,21 +746,37 @@ describe('merge', function()
'@@ -1,1 +1,1 @@',
'-local x = 1',
'+local x = 2',
})
}, working_path)
vim.api.nvim_set_current_buf(d_bufnr)
vim.api.nvim_win_set_cursor(0, { 5, 0 })
merge.setup_keymaps(d_bufnr, default_config({ show_virtual_text = false }))
local cfg = default_config()
merge.resolve_ours(d_bufnr, cfg)
assert.is_true(merge.is_resolved(d_bufnr, 1))
local extmarks =
vim.api.nvim_buf_get_extmarks(d_bufnr, merge.get_namespace(), 0, -1, { details = true })
local virt_text_count = 0
assert.is_true(#extmarks > 0)
merge.setup_keymaps(d_bufnr, cfg)
assert.is_false(merge.is_resolved(d_bufnr, 1))
extmarks =
vim.api.nvim_buf_get_extmarks(d_bufnr, merge.get_namespace(), 0, -1, { details = true })
local resolved_count = 0
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].virt_text then
virt_text_count = virt_text_count + 1
for _, chunk in ipairs(mark[4].virt_text) do
if chunk[1]:match('resolved') then
resolved_count = resolved_count + 1
end
end
end
end
assert.are.equal(0, virt_text_count)
assert.are.equal(0, resolved_count)
helpers.delete_buffer(d_bufnr)
helpers.delete_buffer(w_bufnr)
end)
end)
@ -694,18 +797,6 @@ describe('merge', function()
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('parse_file_line returns status for modified files', function()
local fugitive = require('diffs.fugitive')
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, {
'Unstaged (1)',
'M file.lua',
})
local _, _, _, _, status = fugitive.get_file_at_line(buf, 2)
assert.are.equal('M', status)
vim.api.nvim_buf_delete(buf, { force = true })
end)
it('walkback from hunk line propagates status', function()
local fugitive = require('diffs.fugitive')
local buf = vim.api.nvim_create_buf(false, true)

View file

@ -391,37 +391,6 @@ describe('parser', function()
vim.fn.delete(repo_root, 'rf')
end)
it('detects python from shebang without open buffer', function()
local repo_root = '/tmp/diffs-test-shebang-py'
vim.fn.mkdir(repo_root, 'p')
local file_path = repo_root .. '/deploy'
local f = io.open(file_path, 'w')
f:write('#!/usr/bin/env python3\n')
f:write('import sys\n')
f:write('print("hi")\n')
f:close()
local diff_buf = create_buffer({
'M deploy',
'@@ -1,2 +1,3 @@',
' #!/usr/bin/env python3',
'+import sys',
' print("hi")',
})
vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root)
local hunks = parser.parse_buffer(diff_buf)
assert.are.equal(1, #hunks)
assert.are.equal('deploy', hunks[1].filename)
assert.are.equal('python', hunks[1].ft)
delete_buffer(diff_buf)
os.remove(file_path)
vim.fn.delete(repo_root, 'rf')
end)
it('extracts file line numbers from @@ header', function()
local bufnr = create_buffer({
'M lua/test.lua',
@ -440,22 +409,6 @@ describe('parser', function()
delete_buffer(bufnr)
end)
it('extracts large line numbers from @@ header', function()
local bufnr = create_buffer({
'M lua/test.lua',
'@@ -100,20 +200,30 @@',
' local M = {}',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.are.equal(100, hunks[1].file_old_start)
assert.are.equal(20, hunks[1].file_old_count)
assert.are.equal(200, hunks[1].file_new_start)
assert.are.equal(30, hunks[1].file_new_count)
delete_buffer(bufnr)
end)
it('defaults count to 1 when omitted in @@ header', function()
local bufnr = create_buffer({
'M lua/test.lua',
@ -487,18 +440,5 @@ describe('parser', function()
assert.are.equal('/tmp/test-repo', hunks[1].repo_root)
delete_buffer(bufnr)
end)
it('repo_root is nil when not available', function()
local bufnr = create_buffer({
'M lua/test.lua',
'@@ -1,3 +1,4 @@',
' local M = {}',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
assert.is_nil(hunks[1].repo_root)
delete_buffer(bufnr)
end)
end)
end)