feat(conflict): detect and resolve inline merge conflict markers
Problem: when git hits a merge conflict, users stare at raw <<<<<<< markers with broken treesitter and noisy LSP diagnostics. Existing solutions (git-conflict.nvim) use their own highlighting rather than integrating with diffs.nvim's color blending pipeline. Solution: add conflict.lua module that detects <<<<<<</=======/>>>>>>> markers (with diff3 ||||||| support), highlights ours/theirs/base regions with blended DiffsConflict* highlight groups, provides resolution keymaps (doo/dot/dob/don) and navigation (]x/[x), suppresses diagnostics while markers are present, and auto-detaches when all conflicts are resolved. Fires DiffsConflictResolved user event on last resolution.
This commit is contained in:
parent
e06d22936c
commit
731222d027
4 changed files with 1210 additions and 0 deletions
457
lua/diffs/conflict.lua
Normal file
457
lua/diffs/conflict.lua
Normal file
|
|
@ -0,0 +1,457 @@
|
||||||
|
---@class diffs.ConflictKeymaps
|
||||||
|
---@field ours string|false
|
||||||
|
---@field theirs string|false
|
||||||
|
---@field both string|false
|
||||||
|
---@field none string|false
|
||||||
|
---@field next string|false
|
||||||
|
---@field prev string|false
|
||||||
|
|
||||||
|
---@class diffs.ConflictConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field disable_diagnostics boolean
|
||||||
|
---@field show_virtual_text boolean
|
||||||
|
---@field keymaps diffs.ConflictKeymaps
|
||||||
|
|
||||||
|
---@class diffs.ConflictRegion
|
||||||
|
---@field marker_ours integer
|
||||||
|
---@field ours_start integer
|
||||||
|
---@field ours_end integer
|
||||||
|
---@field marker_base integer?
|
||||||
|
---@field base_start integer?
|
||||||
|
---@field base_end integer?
|
||||||
|
---@field marker_sep integer
|
||||||
|
---@field theirs_start integer
|
||||||
|
---@field theirs_end integer
|
||||||
|
---@field marker_theirs integer
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local ns = vim.api.nvim_create_namespace('diffs-conflict')
|
||||||
|
|
||||||
|
---@type table<integer, true>
|
||||||
|
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)
|
||||||
|
local regions = {}
|
||||||
|
local state = 'idle'
|
||||||
|
local current = nil
|
||||||
|
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
local idx = i - 1
|
||||||
|
|
||||||
|
if state == 'idle' then
|
||||||
|
if line:match('^<<<<<<<') then
|
||||||
|
current = { marker_ours = idx, ours_start = idx + 1 }
|
||||||
|
state = 'in_ours'
|
||||||
|
end
|
||||||
|
elseif state == 'in_ours' then
|
||||||
|
if line:match('^|||||||') then
|
||||||
|
current.ours_end = idx
|
||||||
|
current.marker_base = idx
|
||||||
|
current.base_start = idx + 1
|
||||||
|
state = 'in_base'
|
||||||
|
elseif line:match('^=======') then
|
||||||
|
current.ours_end = idx
|
||||||
|
current.marker_sep = idx
|
||||||
|
current.theirs_start = idx + 1
|
||||||
|
state = 'in_theirs'
|
||||||
|
elseif line:match('^<<<<<<<') then
|
||||||
|
current = { marker_ours = idx, ours_start = idx + 1 }
|
||||||
|
elseif line:match('^>>>>>>>') then
|
||||||
|
current = nil
|
||||||
|
state = 'idle'
|
||||||
|
end
|
||||||
|
elseif state == 'in_base' then
|
||||||
|
if line:match('^=======') then
|
||||||
|
current.base_end = idx
|
||||||
|
current.marker_sep = idx
|
||||||
|
current.theirs_start = idx + 1
|
||||||
|
state = 'in_theirs'
|
||||||
|
elseif line:match('^<<<<<<<') then
|
||||||
|
current = { marker_ours = idx, ours_start = idx + 1 }
|
||||||
|
state = 'in_ours'
|
||||||
|
elseif line:match('^>>>>>>>') then
|
||||||
|
current = nil
|
||||||
|
state = 'idle'
|
||||||
|
end
|
||||||
|
elseif state == 'in_theirs' then
|
||||||
|
if line:match('^>>>>>>>') then
|
||||||
|
current.theirs_end = idx
|
||||||
|
current.marker_theirs = idx
|
||||||
|
table.insert(regions, current)
|
||||||
|
current = nil
|
||||||
|
state = 'idle'
|
||||||
|
elseif line:match('^<<<<<<<') then
|
||||||
|
current = { marker_ours = idx, ours_start = idx + 1 }
|
||||||
|
state = 'in_ours'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return regions
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@return diffs.ConflictRegion[]
|
||||||
|
local function parse_buffer(bufnr)
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
return M.parse(lines)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param regions diffs.ConflictRegion[]
|
||||||
|
---@param config diffs.ConflictConfig
|
||||||
|
local function apply_highlights(bufnr, regions, config)
|
||||||
|
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
||||||
|
|
||||||
|
for _, region in ipairs(regions) do
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, {
|
||||||
|
end_row = region.marker_ours + 1,
|
||||||
|
hl_group = 'DiffsConflictMarker',
|
||||||
|
hl_eol = true,
|
||||||
|
priority = PRIORITY_LINE_BG,
|
||||||
|
})
|
||||||
|
|
||||||
|
if config.show_virtual_text then
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, {
|
||||||
|
virt_text = { { ' current', 'DiffsConflictMarker' } },
|
||||||
|
virt_text_pos = 'eol',
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
for line = region.ours_start, region.ours_end - 1 do
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||||
|
end_row = line + 1,
|
||||||
|
hl_group = 'DiffsConflictOurs',
|
||||||
|
hl_eol = true,
|
||||||
|
priority = PRIORITY_LINE_BG,
|
||||||
|
})
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||||
|
number_hl_group = 'DiffsConflictOursNr',
|
||||||
|
priority = PRIORITY_LINE_BG,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if region.marker_base then
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_base, 0, {
|
||||||
|
end_row = region.marker_base + 1,
|
||||||
|
hl_group = 'DiffsConflictMarker',
|
||||||
|
hl_eol = true,
|
||||||
|
priority = PRIORITY_LINE_BG,
|
||||||
|
})
|
||||||
|
|
||||||
|
for line = region.base_start, region.base_end - 1 do
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||||
|
end_row = line + 1,
|
||||||
|
hl_group = 'DiffsConflictBase',
|
||||||
|
hl_eol = true,
|
||||||
|
priority = PRIORITY_LINE_BG,
|
||||||
|
})
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||||
|
number_hl_group = 'DiffsConflictBaseNr',
|
||||||
|
priority = PRIORITY_LINE_BG,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_sep, 0, {
|
||||||
|
end_row = region.marker_sep + 1,
|
||||||
|
hl_group = 'DiffsConflictMarker',
|
||||||
|
hl_eol = true,
|
||||||
|
priority = PRIORITY_LINE_BG,
|
||||||
|
})
|
||||||
|
|
||||||
|
for line = region.theirs_start, region.theirs_end - 1 do
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||||
|
end_row = line + 1,
|
||||||
|
hl_group = 'DiffsConflictTheirs',
|
||||||
|
hl_eol = true,
|
||||||
|
priority = PRIORITY_LINE_BG,
|
||||||
|
})
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||||
|
number_hl_group = 'DiffsConflictTheirsNr',
|
||||||
|
priority = PRIORITY_LINE_BG,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, {
|
||||||
|
end_row = region.marker_theirs + 1,
|
||||||
|
hl_group = 'DiffsConflictMarker',
|
||||||
|
hl_eol = true,
|
||||||
|
priority = PRIORITY_LINE_BG,
|
||||||
|
})
|
||||||
|
|
||||||
|
if config.show_virtual_text then
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, {
|
||||||
|
virt_text = { { ' incoming', 'DiffsConflictMarker' } },
|
||||||
|
virt_text_pos = 'eol',
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param cursor_line integer
|
||||||
|
---@param regions diffs.ConflictRegion[]
|
||||||
|
---@return diffs.ConflictRegion?
|
||||||
|
local function find_conflict_at_cursor(cursor_line, regions)
|
||||||
|
for _, region in ipairs(regions) do
|
||||||
|
if cursor_line >= region.marker_ours and cursor_line <= region.marker_theirs then
|
||||||
|
return region
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param region diffs.ConflictRegion
|
||||||
|
---@param replacement string[]
|
||||||
|
local function replace_region(bufnr, region, replacement)
|
||||||
|
vim.api.nvim_buf_set_lines(
|
||||||
|
bufnr,
|
||||||
|
region.marker_ours,
|
||||||
|
region.marker_theirs + 1,
|
||||||
|
false,
|
||||||
|
replacement
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param config diffs.ConflictConfig
|
||||||
|
local function refresh(bufnr, config)
|
||||||
|
local regions = parse_buffer(bufnr)
|
||||||
|
if #regions == 0 then
|
||||||
|
M.detach(bufnr)
|
||||||
|
vim.api.nvim_exec_autocmds('User', { pattern = 'DiffsConflictResolved' })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
apply_highlights(bufnr, regions, config)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param config diffs.ConflictConfig
|
||||||
|
function M.resolve_ours(bufnr, config)
|
||||||
|
if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then
|
||||||
|
vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local regions = parse_buffer(bufnr)
|
||||||
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
|
local region = find_conflict_at_cursor(cursor[1] - 1, regions)
|
||||||
|
if not region then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, region.ours_start, region.ours_end, false)
|
||||||
|
replace_region(bufnr, region, lines)
|
||||||
|
refresh(bufnr, config)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param config diffs.ConflictConfig
|
||||||
|
function M.resolve_theirs(bufnr, config)
|
||||||
|
if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then
|
||||||
|
vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local regions = parse_buffer(bufnr)
|
||||||
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
|
local region = find_conflict_at_cursor(cursor[1] - 1, regions)
|
||||||
|
if not region then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, region.theirs_start, region.theirs_end, false)
|
||||||
|
replace_region(bufnr, region, lines)
|
||||||
|
refresh(bufnr, config)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param config diffs.ConflictConfig
|
||||||
|
function M.resolve_both(bufnr, config)
|
||||||
|
if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then
|
||||||
|
vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local regions = parse_buffer(bufnr)
|
||||||
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
|
local region = find_conflict_at_cursor(cursor[1] - 1, regions)
|
||||||
|
if not region then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ours = vim.api.nvim_buf_get_lines(bufnr, region.ours_start, region.ours_end, false)
|
||||||
|
local theirs = vim.api.nvim_buf_get_lines(bufnr, region.theirs_start, region.theirs_end, false)
|
||||||
|
local combined = {}
|
||||||
|
for _, l in ipairs(ours) do
|
||||||
|
table.insert(combined, l)
|
||||||
|
end
|
||||||
|
for _, l in ipairs(theirs) do
|
||||||
|
table.insert(combined, l)
|
||||||
|
end
|
||||||
|
replace_region(bufnr, region, combined)
|
||||||
|
refresh(bufnr, config)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param config diffs.ConflictConfig
|
||||||
|
function M.resolve_none(bufnr, config)
|
||||||
|
if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then
|
||||||
|
vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local regions = parse_buffer(bufnr)
|
||||||
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
|
local region = find_conflict_at_cursor(cursor[1] - 1, regions)
|
||||||
|
if not region then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
replace_region(bufnr, region, {})
|
||||||
|
refresh(bufnr, config)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
function M.goto_next(bufnr)
|
||||||
|
local regions = parse_buffer(bufnr)
|
||||||
|
if #regions == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
|
local cursor_line = cursor[1] - 1
|
||||||
|
for _, region in ipairs(regions) do
|
||||||
|
if region.marker_ours > cursor_line then
|
||||||
|
vim.api.nvim_win_set_cursor(0, { region.marker_ours + 1, 0 })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vim.api.nvim_win_set_cursor(0, { regions[1].marker_ours + 1, 0 })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
function M.goto_prev(bufnr)
|
||||||
|
local regions = parse_buffer(bufnr)
|
||||||
|
if #regions == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
|
local cursor_line = cursor[1] - 1
|
||||||
|
for i = #regions, 1, -1 do
|
||||||
|
if regions[i].marker_ours < cursor_line then
|
||||||
|
vim.api.nvim_win_set_cursor(0, { regions[i].marker_ours + 1, 0 })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vim.api.nvim_win_set_cursor(0, { regions[#regions].marker_ours + 1, 0 })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param config diffs.ConflictConfig
|
||||||
|
local function setup_keymaps(bufnr, config)
|
||||||
|
local km = config.keymaps
|
||||||
|
|
||||||
|
if km.ours then
|
||||||
|
vim.keymap.set('n', km.ours, function()
|
||||||
|
M.resolve_ours(bufnr, config)
|
||||||
|
end, { buffer = bufnr })
|
||||||
|
end
|
||||||
|
if km.theirs then
|
||||||
|
vim.keymap.set('n', km.theirs, function()
|
||||||
|
M.resolve_theirs(bufnr, config)
|
||||||
|
end, { buffer = bufnr })
|
||||||
|
end
|
||||||
|
if km.both then
|
||||||
|
vim.keymap.set('n', km.both, function()
|
||||||
|
M.resolve_both(bufnr, config)
|
||||||
|
end, { buffer = bufnr })
|
||||||
|
end
|
||||||
|
if km.none then
|
||||||
|
vim.keymap.set('n', km.none, function()
|
||||||
|
M.resolve_none(bufnr, config)
|
||||||
|
end, { buffer = bufnr })
|
||||||
|
end
|
||||||
|
if km.next then
|
||||||
|
vim.keymap.set('n', km.next, function()
|
||||||
|
M.goto_next(bufnr)
|
||||||
|
end, { buffer = bufnr })
|
||||||
|
end
|
||||||
|
if km.prev then
|
||||||
|
vim.keymap.set('n', km.prev, function()
|
||||||
|
M.goto_prev(bufnr)
|
||||||
|
end, { buffer = bufnr })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
function M.detach(bufnr)
|
||||||
|
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
||||||
|
attached_buffers[bufnr] = nil
|
||||||
|
|
||||||
|
if diagnostics_suppressed[bufnr] then
|
||||||
|
pcall(vim.diagnostic.enable, true, { bufnr = bufnr })
|
||||||
|
diagnostics_suppressed[bufnr] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param config diffs.ConflictConfig
|
||||||
|
function M.attach(bufnr, config)
|
||||||
|
if attached_buffers[bufnr] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local buftype = vim.api.nvim_get_option_value('buftype', { buf = bufnr })
|
||||||
|
if buftype ~= '' then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
local has_marker = false
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
if line:match('^<<<<<<<') then
|
||||||
|
has_marker = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not has_marker then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
attached_buffers[bufnr] = true
|
||||||
|
|
||||||
|
local regions = M.parse(lines)
|
||||||
|
apply_highlights(bufnr, regions, config)
|
||||||
|
setup_keymaps(bufnr, config)
|
||||||
|
|
||||||
|
if config.disable_diagnostics then
|
||||||
|
pcall(vim.diagnostic.enable, false, { bufnr = bufnr })
|
||||||
|
diagnostics_suppressed[bufnr] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
|
||||||
|
buffer = bufnr,
|
||||||
|
callback = function()
|
||||||
|
if not attached_buffers[bufnr] then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
refresh(bufnr, config)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd('BufWipeout', {
|
||||||
|
buffer = bufnr,
|
||||||
|
callback = function()
|
||||||
|
attached_buffers[bufnr] = nil
|
||||||
|
diagnostics_suppressed[bufnr] = nil
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return integer
|
||||||
|
function M.get_namespace()
|
||||||
|
return ns
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
|
|
@ -29,12 +29,27 @@
|
||||||
---@field horizontal string|false
|
---@field horizontal string|false
|
||||||
---@field vertical string|false
|
---@field vertical string|false
|
||||||
|
|
||||||
|
---@class diffs.ConflictKeymaps
|
||||||
|
---@field ours string|false
|
||||||
|
---@field theirs string|false
|
||||||
|
---@field both string|false
|
||||||
|
---@field none string|false
|
||||||
|
---@field next string|false
|
||||||
|
---@field prev string|false
|
||||||
|
|
||||||
|
---@class diffs.ConflictConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field disable_diagnostics boolean
|
||||||
|
---@field show_virtual_text boolean
|
||||||
|
---@field keymaps diffs.ConflictKeymaps
|
||||||
|
|
||||||
---@class diffs.Config
|
---@class diffs.Config
|
||||||
---@field debug boolean
|
---@field debug boolean
|
||||||
---@field debounce_ms integer
|
---@field debounce_ms integer
|
||||||
---@field hide_prefix boolean
|
---@field hide_prefix boolean
|
||||||
---@field highlights diffs.Highlights
|
---@field highlights diffs.Highlights
|
||||||
---@field fugitive diffs.FugitiveConfig
|
---@field fugitive diffs.FugitiveConfig
|
||||||
|
---@field conflict diffs.ConflictConfig
|
||||||
|
|
||||||
---@class diffs
|
---@class diffs
|
||||||
---@field attach fun(bufnr?: integer)
|
---@field attach fun(bufnr?: integer)
|
||||||
|
|
@ -109,6 +124,19 @@ local default_config = {
|
||||||
horizontal = 'du',
|
horizontal = 'du',
|
||||||
vertical = 'dU',
|
vertical = 'dU',
|
||||||
},
|
},
|
||||||
|
conflict = {
|
||||||
|
enabled = true,
|
||||||
|
disable_diagnostics = true,
|
||||||
|
show_virtual_text = true,
|
||||||
|
keymaps = {
|
||||||
|
ours = 'doo',
|
||||||
|
theirs = 'dot',
|
||||||
|
both = 'dob',
|
||||||
|
none = 'don',
|
||||||
|
next = ']x',
|
||||||
|
prev = '[x',
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
---@type diffs.Config
|
---@type diffs.Config
|
||||||
|
|
@ -231,6 +259,37 @@ local function compute_highlight_groups()
|
||||||
vim.api.nvim_set_hl(0, 'DiffsDiffChange', { default = true, bg = diff_change.bg })
|
vim.api.nvim_set_hl(0, 'DiffsDiffChange', { default = true, bg = diff_change.bg })
|
||||||
vim.api.nvim_set_hl(0, 'DiffsDiffText', { default = true, bg = diff_text.bg })
|
vim.api.nvim_set_hl(0, 'DiffsDiffText', { default = true, bg = diff_text.bg })
|
||||||
|
|
||||||
|
local change_bg = diff_change.bg or 0x3a3a4a
|
||||||
|
local text_bg = diff_text.bg or 0x4a4a5a
|
||||||
|
local change_fg = diff_change.fg or diff_text.fg or 0x80a0c0
|
||||||
|
|
||||||
|
local blended_ours = blend_color(add_bg, bg, 0.4)
|
||||||
|
local blended_theirs = blend_color(change_bg, bg, 0.4)
|
||||||
|
local blended_base = blend_color(text_bg, bg, 0.3)
|
||||||
|
local blended_ours_nr = blend_color(add_fg, bg, alpha)
|
||||||
|
local blended_theirs_nr = blend_color(change_fg, bg, alpha)
|
||||||
|
local blended_base_nr = blend_color(change_fg, bg, 0.4)
|
||||||
|
|
||||||
|
vim.api.nvim_set_hl(0, 'DiffsConflictOurs', { default = true, bg = blended_ours })
|
||||||
|
vim.api.nvim_set_hl(0, 'DiffsConflictTheirs', { default = true, bg = blended_theirs })
|
||||||
|
vim.api.nvim_set_hl(0, 'DiffsConflictBase', { default = true, bg = blended_base })
|
||||||
|
vim.api.nvim_set_hl(0, 'DiffsConflictMarker', { default = true, fg = 0x808080, bold = true })
|
||||||
|
vim.api.nvim_set_hl(
|
||||||
|
0,
|
||||||
|
'DiffsConflictOursNr',
|
||||||
|
{ default = true, fg = blended_ours_nr, bg = blended_ours }
|
||||||
|
)
|
||||||
|
vim.api.nvim_set_hl(
|
||||||
|
0,
|
||||||
|
'DiffsConflictTheirsNr',
|
||||||
|
{ default = true, fg = blended_theirs_nr, bg = blended_theirs }
|
||||||
|
)
|
||||||
|
vim.api.nvim_set_hl(
|
||||||
|
0,
|
||||||
|
'DiffsConflictBaseNr',
|
||||||
|
{ default = true, fg = blended_base_nr, bg = blended_base }
|
||||||
|
)
|
||||||
|
|
||||||
if config.highlights.overrides then
|
if config.highlights.overrides then
|
||||||
for group, hl in pairs(config.highlights.overrides) do
|
for group, hl in pairs(config.highlights.overrides) do
|
||||||
vim.api.nvim_set_hl(0, group, hl)
|
vim.api.nvim_set_hl(0, group, hl)
|
||||||
|
|
@ -324,6 +383,30 @@ local function init()
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if opts.conflict then
|
||||||
|
vim.validate({
|
||||||
|
['conflict.enabled'] = { opts.conflict.enabled, 'boolean', true },
|
||||||
|
['conflict.disable_diagnostics'] = { opts.conflict.disable_diagnostics, 'boolean', true },
|
||||||
|
['conflict.show_virtual_text'] = { opts.conflict.show_virtual_text, 'boolean', true },
|
||||||
|
['conflict.keymaps'] = { opts.conflict.keymaps, 'table', true },
|
||||||
|
})
|
||||||
|
|
||||||
|
if opts.conflict.keymaps then
|
||||||
|
local keymap_validator = function(v)
|
||||||
|
return v == false or type(v) == 'string'
|
||||||
|
end
|
||||||
|
for _, key in ipairs({ 'ours', 'theirs', 'both', 'none', 'next', 'prev' }) do
|
||||||
|
vim.validate({
|
||||||
|
['conflict.keymaps.' .. key] = {
|
||||||
|
opts.conflict.keymaps[key],
|
||||||
|
keymap_validator,
|
||||||
|
'string or false',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if opts.debounce_ms and opts.debounce_ms < 0 then
|
if opts.debounce_ms and opts.debounce_ms < 0 then
|
||||||
error('diffs: debounce_ms must be >= 0')
|
error('diffs: debounce_ms must be >= 0')
|
||||||
end
|
end
|
||||||
|
|
@ -488,4 +571,10 @@ function M.get_fugitive_config()
|
||||||
return config.fugitive
|
return config.fugitive
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return diffs.ConflictConfig
|
||||||
|
function M.get_conflict_config()
|
||||||
|
init()
|
||||||
|
return config.conflict
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,15 @@ vim.api.nvim_create_autocmd('BufReadCmd', {
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd('BufReadPost', {
|
||||||
|
callback = function(args)
|
||||||
|
local conflict_config = require('diffs').get_conflict_config()
|
||||||
|
if conflict_config.enabled then
|
||||||
|
require('diffs.conflict').attach(args.buf, conflict_config)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd('OptionSet', {
|
vim.api.nvim_create_autocmd('OptionSet', {
|
||||||
pattern = 'diff',
|
pattern = 'diff',
|
||||||
callback = function()
|
callback = function()
|
||||||
|
|
|
||||||
655
spec/conflict_spec.lua
Normal file
655
spec/conflict_spec.lua
Normal file
|
|
@ -0,0 +1,655 @@
|
||||||
|
local conflict = require('diffs.conflict')
|
||||||
|
local helpers = require('spec.helpers')
|
||||||
|
|
||||||
|
local function default_config(overrides)
|
||||||
|
local cfg = {
|
||||||
|
enabled = true,
|
||||||
|
disable_diagnostics = false,
|
||||||
|
show_virtual_text = true,
|
||||||
|
keymaps = {
|
||||||
|
ours = 'doo',
|
||||||
|
theirs = 'dot',
|
||||||
|
both = 'dob',
|
||||||
|
none = 'don',
|
||||||
|
next = ']x',
|
||||||
|
prev = '[x',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if overrides then
|
||||||
|
cfg = vim.tbl_deep_extend('force', cfg, overrides)
|
||||||
|
end
|
||||||
|
return cfg
|
||||||
|
end
|
||||||
|
|
||||||
|
local function create_file_buffer(lines)
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, false)
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {})
|
||||||
|
return bufnr
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_extmarks(bufnr)
|
||||||
|
return vim.api.nvim_buf_get_extmarks(bufnr, conflict.get_namespace(), 0, -1, { details = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
describe('conflict', function()
|
||||||
|
describe('parse', function()
|
||||||
|
it('parses a single conflict', function()
|
||||||
|
local lines = {
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
}
|
||||||
|
local regions = conflict.parse(lines)
|
||||||
|
assert.are.equal(1, #regions)
|
||||||
|
assert.are.equal(0, regions[1].marker_ours)
|
||||||
|
assert.are.equal(1, regions[1].ours_start)
|
||||||
|
assert.are.equal(2, regions[1].ours_end)
|
||||||
|
assert.are.equal(2, regions[1].marker_sep)
|
||||||
|
assert.are.equal(3, regions[1].theirs_start)
|
||||||
|
assert.are.equal(4, regions[1].theirs_end)
|
||||||
|
assert.are.equal(4, regions[1].marker_theirs)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('parses multiple conflicts', function()
|
||||||
|
local lines = {
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'a',
|
||||||
|
'=======',
|
||||||
|
'b',
|
||||||
|
'>>>>>>> feat',
|
||||||
|
'normal line',
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'c',
|
||||||
|
'=======',
|
||||||
|
'd',
|
||||||
|
'>>>>>>> feat',
|
||||||
|
}
|
||||||
|
local regions = conflict.parse(lines)
|
||||||
|
assert.are.equal(2, #regions)
|
||||||
|
assert.are.equal(0, regions[1].marker_ours)
|
||||||
|
assert.are.equal(6, regions[2].marker_ours)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('parses diff3 format', function()
|
||||||
|
local lines = {
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'||||||| base',
|
||||||
|
'local x = 0',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
}
|
||||||
|
local regions = conflict.parse(lines)
|
||||||
|
assert.are.equal(1, #regions)
|
||||||
|
assert.are.equal(2, regions[1].marker_base)
|
||||||
|
assert.are.equal(3, regions[1].base_start)
|
||||||
|
assert.are.equal(4, regions[1].base_end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('handles empty ours section', function()
|
||||||
|
local lines = {
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
}
|
||||||
|
local regions = conflict.parse(lines)
|
||||||
|
assert.are.equal(1, #regions)
|
||||||
|
assert.are.equal(1, regions[1].ours_start)
|
||||||
|
assert.are.equal(1, regions[1].ours_end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('handles empty theirs section', function()
|
||||||
|
local lines = {
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'=======',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
}
|
||||||
|
local regions = conflict.parse(lines)
|
||||||
|
assert.are.equal(1, #regions)
|
||||||
|
assert.are.equal(3, regions[1].theirs_start)
|
||||||
|
assert.are.equal(3, regions[1].theirs_end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns empty for no markers', function()
|
||||||
|
local lines = { 'local x = 1', 'local y = 2' }
|
||||||
|
local regions = conflict.parse(lines)
|
||||||
|
assert.are.equal(0, #regions)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('discards malformed markers (no separator)', function()
|
||||||
|
local lines = {
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
}
|
||||||
|
local regions = conflict.parse(lines)
|
||||||
|
assert.are.equal(0, #regions)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('discards malformed markers (no end)', function()
|
||||||
|
local lines = {
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
}
|
||||||
|
local regions = conflict.parse(lines)
|
||||||
|
assert.are.equal(0, #regions)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('handles trailing text on marker lines', function()
|
||||||
|
local lines = {
|
||||||
|
'<<<<<<< HEAD (some text)',
|
||||||
|
'local x = 1',
|
||||||
|
'======= extra',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature-branch/some-thing',
|
||||||
|
}
|
||||||
|
local regions = conflict.parse(lines)
|
||||||
|
assert.are.equal(1, #regions)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('handles empty base in diff3', function()
|
||||||
|
local lines = {
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'||||||| base',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
}
|
||||||
|
local regions = conflict.parse(lines)
|
||||||
|
assert.are.equal(1, #regions)
|
||||||
|
assert.are.equal(3, regions[1].base_start)
|
||||||
|
assert.are.equal(3, regions[1].base_end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('highlighting', function()
|
||||||
|
after_each(function()
|
||||||
|
conflict.detach(vim.api.nvim_get_current_buf())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('applies extmarks for conflict regions', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
})
|
||||||
|
|
||||||
|
conflict.attach(bufnr, default_config())
|
||||||
|
|
||||||
|
local extmarks = get_extmarks(bufnr)
|
||||||
|
assert.is_true(#extmarks > 0)
|
||||||
|
|
||||||
|
local has_ours = false
|
||||||
|
local has_theirs = false
|
||||||
|
local has_marker = false
|
||||||
|
for _, mark in ipairs(extmarks) do
|
||||||
|
local hl = mark[4] and mark[4].hl_group
|
||||||
|
if hl == 'DiffsConflictOurs' then
|
||||||
|
has_ours = true
|
||||||
|
end
|
||||||
|
if hl == 'DiffsConflictTheirs' then
|
||||||
|
has_theirs = true
|
||||||
|
end
|
||||||
|
if hl == 'DiffsConflictMarker' then
|
||||||
|
has_marker = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(has_ours)
|
||||||
|
assert.is_true(has_theirs)
|
||||||
|
assert.is_true(has_marker)
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('applies virtual text when enabled', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
})
|
||||||
|
|
||||||
|
conflict.attach(bufnr, default_config({ show_virtual_text = true }))
|
||||||
|
|
||||||
|
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(2, virt_text_count)
|
||||||
|
|
||||||
|
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',
|
||||||
|
'local x = 1',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
})
|
||||||
|
|
||||||
|
conflict.attach(bufnr, default_config())
|
||||||
|
|
||||||
|
local extmarks = get_extmarks(bufnr)
|
||||||
|
local has_ours_nr = false
|
||||||
|
local has_theirs_nr = false
|
||||||
|
for _, mark in ipairs(extmarks) do
|
||||||
|
local nr = mark[4] and mark[4].number_hl_group
|
||||||
|
if nr == 'DiffsConflictOursNr' then
|
||||||
|
has_ours_nr = true
|
||||||
|
end
|
||||||
|
if nr == 'DiffsConflictTheirsNr' then
|
||||||
|
has_theirs_nr = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(has_ours_nr)
|
||||||
|
assert.is_true(has_theirs_nr)
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('highlights base region in diff3', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'||||||| base',
|
||||||
|
'local x = 0',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
})
|
||||||
|
|
||||||
|
conflict.attach(bufnr, default_config())
|
||||||
|
|
||||||
|
local extmarks = get_extmarks(bufnr)
|
||||||
|
local has_base = false
|
||||||
|
for _, mark in ipairs(extmarks) do
|
||||||
|
if mark[4] and mark[4].hl_group == 'DiffsConflictBase' then
|
||||||
|
has_base = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(has_base)
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('clears extmarks on detach', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
})
|
||||||
|
|
||||||
|
conflict.attach(bufnr, default_config())
|
||||||
|
assert.is_true(#get_extmarks(bufnr) > 0)
|
||||||
|
|
||||||
|
conflict.detach(bufnr)
|
||||||
|
assert.are.equal(0, #get_extmarks(bufnr))
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('resolution', function()
|
||||||
|
local function make_conflict_buffer()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
})
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
return bufnr
|
||||||
|
end
|
||||||
|
|
||||||
|
it('resolve_ours keeps ours content', function()
|
||||||
|
local bufnr = make_conflict_buffer()
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
|
||||||
|
conflict.resolve_ours(bufnr, default_config())
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
assert.are.equal(1, #lines)
|
||||||
|
assert.are.equal('local x = 1', lines[1])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('resolve_theirs keeps theirs content', function()
|
||||||
|
local bufnr = make_conflict_buffer()
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
|
||||||
|
conflict.resolve_theirs(bufnr, default_config())
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
assert.are.equal(1, #lines)
|
||||||
|
assert.are.equal('local x = 2', lines[1])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('resolve_both keeps ours then theirs', function()
|
||||||
|
local bufnr = make_conflict_buffer()
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
|
||||||
|
conflict.resolve_both(bufnr, default_config())
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
assert.are.equal(2, #lines)
|
||||||
|
assert.are.equal('local x = 1', lines[1])
|
||||||
|
assert.are.equal('local x = 2', lines[2])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('resolve_none removes entire block', function()
|
||||||
|
local bufnr = make_conflict_buffer()
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
|
||||||
|
conflict.resolve_none(bufnr, default_config())
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
assert.are.equal(1, #lines)
|
||||||
|
assert.are.equal('', lines[1])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('does nothing when cursor is outside conflict', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'normal line',
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
})
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
|
||||||
|
conflict.resolve_ours(bufnr, default_config())
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
assert.are.equal(6, #lines)
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('resolves one conflict among multiple', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'a',
|
||||||
|
'=======',
|
||||||
|
'b',
|
||||||
|
'>>>>>>> feat',
|
||||||
|
'middle',
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'c',
|
||||||
|
'=======',
|
||||||
|
'd',
|
||||||
|
'>>>>>>> feat',
|
||||||
|
})
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
|
||||||
|
conflict.resolve_ours(bufnr, default_config())
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
assert.are.equal('a', lines[1])
|
||||||
|
assert.are.equal('middle', lines[2])
|
||||||
|
assert.are.equal('<<<<<<< HEAD', lines[3])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('resolve_ours with empty ours section', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
})
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
|
||||||
|
conflict.resolve_ours(bufnr, default_config())
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
assert.are.equal(1, #lines)
|
||||||
|
assert.are.equal('', lines[1])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('handles diff3 resolution (ignores base)', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'||||||| base',
|
||||||
|
'local x = 0',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
})
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
|
||||||
|
conflict.resolve_theirs(bufnr, default_config())
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
assert.are.equal(1, #lines)
|
||||||
|
assert.are.equal('local x = 2', lines[1])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('navigation', function()
|
||||||
|
it('goto_next jumps to next conflict', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'normal',
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'a',
|
||||||
|
'=======',
|
||||||
|
'b',
|
||||||
|
'>>>>>>> feat',
|
||||||
|
'middle',
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'c',
|
||||||
|
'=======',
|
||||||
|
'd',
|
||||||
|
'>>>>>>> feat',
|
||||||
|
})
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
|
||||||
|
conflict.goto_next(bufnr)
|
||||||
|
assert.are.equal(2, vim.api.nvim_win_get_cursor(0)[1])
|
||||||
|
|
||||||
|
conflict.goto_next(bufnr)
|
||||||
|
assert.are.equal(8, vim.api.nvim_win_get_cursor(0)[1])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('goto_next wraps to first conflict', 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 })
|
||||||
|
|
||||||
|
conflict.goto_next(bufnr)
|
||||||
|
assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('goto_prev jumps to previous conflict', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'a',
|
||||||
|
'=======',
|
||||||
|
'b',
|
||||||
|
'>>>>>>> feat',
|
||||||
|
'middle',
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'c',
|
||||||
|
'=======',
|
||||||
|
'd',
|
||||||
|
'>>>>>>> feat',
|
||||||
|
'end',
|
||||||
|
})
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 12, 0 })
|
||||||
|
|
||||||
|
conflict.goto_prev(bufnr)
|
||||||
|
assert.are.equal(7, vim.api.nvim_win_get_cursor(0)[1])
|
||||||
|
|
||||||
|
conflict.goto_prev(bufnr)
|
||||||
|
assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('goto_prev wraps to last conflict', 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 })
|
||||||
|
|
||||||
|
conflict.goto_prev(bufnr)
|
||||||
|
assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1])
|
||||||
|
|
||||||
|
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)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
|
||||||
|
conflict.goto_next(bufnr)
|
||||||
|
assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1])
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('lifecycle', function()
|
||||||
|
it('attach is idempotent', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'a',
|
||||||
|
'=======',
|
||||||
|
'b',
|
||||||
|
'>>>>>>> feat',
|
||||||
|
})
|
||||||
|
local cfg = default_config()
|
||||||
|
conflict.attach(bufnr, cfg)
|
||||||
|
local count1 = #get_extmarks(bufnr)
|
||||||
|
conflict.attach(bufnr, cfg)
|
||||||
|
local count2 = #get_extmarks(bufnr)
|
||||||
|
assert.are.equal(count1, count2)
|
||||||
|
conflict.detach(bufnr)
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('skips non-file buffers', function()
|
||||||
|
local bufnr = helpers.create_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'a',
|
||||||
|
'=======',
|
||||||
|
'b',
|
||||||
|
'>>>>>>> feat',
|
||||||
|
})
|
||||||
|
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr })
|
||||||
|
|
||||||
|
conflict.attach(bufnr, default_config())
|
||||||
|
assert.are.equal(0, #get_extmarks(bufnr))
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('skips buffers without conflict markers', function()
|
||||||
|
local bufnr = create_file_buffer({ 'local x = 1', 'local y = 2' })
|
||||||
|
|
||||||
|
conflict.attach(bufnr, default_config())
|
||||||
|
assert.are.equal(0, #get_extmarks(bufnr))
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('detaches after last conflict resolved', function()
|
||||||
|
local bufnr = create_file_buffer({
|
||||||
|
'<<<<<<< HEAD',
|
||||||
|
'local x = 1',
|
||||||
|
'=======',
|
||||||
|
'local x = 2',
|
||||||
|
'>>>>>>> feature',
|
||||||
|
})
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
conflict.attach(bufnr, default_config())
|
||||||
|
|
||||||
|
assert.is_true(#get_extmarks(bufnr) > 0)
|
||||||
|
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
conflict.resolve_ours(bufnr, default_config())
|
||||||
|
|
||||||
|
assert.are.equal(0, #get_extmarks(bufnr))
|
||||||
|
|
||||||
|
helpers.delete_buffer(bufnr)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue