diff --git a/README.md b/README.md index ca3d72e..f0a04a8 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ syntax highlighting. - Vim syntax fallback for languages without a treesitter parser - Hunk header context highlighting (`@@ ... @@ function foo()`) - Character-level (intra-line) diff highlighting for changed characters +- Inline merge conflict detection, highlighting, and resolution keymaps - Configurable debouncing, max lines, diff prefix concealment, blend alpha, and highlight overrides @@ -63,7 +64,8 @@ luarocks install diffs.nvim compatible, but both plugins modifying line highlights may produce unexpected results - [`git-conflict.nvim`](https://github.com/akinsho/git-conflict.nvim) - - conflict marker highlighting may overlap with `diffs.nvim` + `diffs.nvim` now includes built-in conflict resolution; disable one or the + other to avoid overlap # Acknowledgements diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index 028d8ba..7663150 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -280,6 +280,41 @@ Example configuration: >lua vim.keymap.set('n', 'gD', '(diffs-gvdiff)') < + *(diffs-conflict-ours)* +(diffs-conflict-ours) + Accept current (ours) change. Replaces the + conflict block with ours content. + + *(diffs-conflict-theirs)* +(diffs-conflict-theirs) + Accept incoming (theirs) change. Replaces the + conflict block with theirs content. + + *(diffs-conflict-both)* +(diffs-conflict-both) + Accept both changes (ours then theirs). + + *(diffs-conflict-none)* +(diffs-conflict-none) + Reject both changes (delete entire block). + + *(diffs-conflict-next)* +(diffs-conflict-next) + Jump to next conflict marker. Wraps around. + + *(diffs-conflict-prev)* +(diffs-conflict-prev) + Jump to previous conflict marker. Wraps around. + +Example configuration: >lua + vim.keymap.set('n', 'co', '(diffs-conflict-ours)') + vim.keymap.set('n', 'ct', '(diffs-conflict-theirs)') + vim.keymap.set('n', 'cb', '(diffs-conflict-both)') + vim.keymap.set('n', 'cn', '(diffs-conflict-none)') + vim.keymap.set('n', ']x', '(diffs-conflict-next)') + vim.keymap.set('n', '[x', '(diffs-conflict-prev)') +< + Diff buffer mappings: ~ *diffs-q* q Close the diff window. Available in all `diffs://` diff --git a/lua/diffs/conflict.lua b/lua/diffs/conflict.lua index 84cc2fa..9e62a15 100644 --- a/lua/diffs/conflict.lua +++ b/lua/diffs/conflict.lua @@ -107,15 +107,8 @@ local function apply_highlights(bufnr, regions, config) }) if config.show_virtual_text then - local ours_line = vim.api.nvim_buf_get_lines( - bufnr, - region.marker_ours, - region.marker_ours + 1, - false - )[1] or '' - local ours_name = ours_line:match('^<<<<<<<%s+(.+)$') or '' pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, { - virt_text = { { ' ' .. ours_name .. ' (current)', 'DiffsConflictMarker' } }, + virt_text = { { ' (current)', 'DiffsConflictMarker' } }, virt_text_pos = 'eol', }) end @@ -183,15 +176,8 @@ local function apply_highlights(bufnr, regions, config) }) if config.show_virtual_text then - local theirs_line = vim.api.nvim_buf_get_lines( - bufnr, - region.marker_theirs, - region.marker_theirs + 1, - false - )[1] or '' - local theirs_name = theirs_line:match('^>>>>>>>%s+(.+)$') or '' pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, { - virt_text = { { ' ' .. theirs_name .. ' (incoming)', 'DiffsConflictMarker' } }, + virt_text = { { ' (incoming)', 'DiffsConflictMarker' } }, virt_text_pos = 'eol', }) end @@ -228,11 +214,20 @@ end local function refresh(bufnr, config) local regions = parse_buffer(bufnr) if #regions == 0 then - M.detach(bufnr) + vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) + if diagnostics_suppressed[bufnr] then + pcall(vim.diagnostic.reset, nil, bufnr) + pcall(vim.diagnostic.enable, true, { bufnr = bufnr }) + diagnostics_suppressed[bufnr] = nil + end vim.api.nvim_exec_autocmds('User', { pattern = 'DiffsConflictResolved' }) return end apply_highlights(bufnr, regions, config) + if config.disable_diagnostics and not diagnostics_suppressed[bufnr] then + pcall(vim.diagnostic.enable, false, { bufnr = bufnr }) + diagnostics_suppressed[bufnr] = true + end end ---@param bufnr integer @@ -353,35 +348,19 @@ end 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 }) + local maps = { + { km.ours, '(diffs-conflict-ours)' }, + { km.theirs, '(diffs-conflict-theirs)' }, + { km.both, '(diffs-conflict-both)' }, + { km.none, '(diffs-conflict-none)' }, + { km.next, '(diffs-conflict-next)' }, + { km.prev, '(diffs-conflict-prev)' }, + } + + for _, map in ipairs(maps) do + if map[1] then + vim.keymap.set('n', map[1], map[2], { buffer = bufnr }) + end end end @@ -391,6 +370,7 @@ function M.detach(bufnr) attached_buffers[bufnr] = nil if diagnostics_suppressed[bufnr] then + pcall(vim.diagnostic.reset, nil, bufnr) pcall(vim.diagnostic.enable, true, { bufnr = bufnr }) diagnostics_suppressed[bufnr] = nil end diff --git a/plugin/diffs.lua b/plugin/diffs.lua index e4fc690..5d3c8b2 100644 --- a/plugin/diffs.lua +++ b/plugin/diffs.lua @@ -57,3 +57,28 @@ end, { desc = 'Unified diff (horizontal)' }) vim.keymap.set('n', '(diffs-gvdiff)', function() cmds.gdiff(nil, true) end, { desc = 'Unified diff (vertical)' }) + +local function conflict_action(fn) + local bufnr = vim.api.nvim_get_current_buf() + local config = require('diffs').get_conflict_config() + fn(bufnr, config) +end + +vim.keymap.set('n', '(diffs-conflict-ours)', function() + conflict_action(require('diffs.conflict').resolve_ours) +end, { desc = 'Accept current (ours) change' }) +vim.keymap.set('n', '(diffs-conflict-theirs)', function() + conflict_action(require('diffs.conflict').resolve_theirs) +end, { desc = 'Accept incoming (theirs) change' }) +vim.keymap.set('n', '(diffs-conflict-both)', function() + conflict_action(require('diffs.conflict').resolve_both) +end, { desc = 'Accept both changes' }) +vim.keymap.set('n', '(diffs-conflict-none)', function() + conflict_action(require('diffs.conflict').resolve_none) +end, { desc = 'Reject both changes' }) +vim.keymap.set('n', '(diffs-conflict-next)', function() + require('diffs.conflict').goto_next(vim.api.nvim_get_current_buf()) +end, { desc = 'Jump to next conflict' }) +vim.keymap.set('n', '(diffs-conflict-prev)', function() + require('diffs.conflict').goto_prev(vim.api.nvim_get_current_buf()) +end, { desc = 'Jump to previous conflict' }) diff --git a/spec/conflict_spec.lua b/spec/conflict_spec.lua index 128c567..75eac23 100644 --- a/spec/conflict_spec.lua +++ b/spec/conflict_spec.lua @@ -631,6 +631,39 @@ describe('conflict', function() helpers.delete_buffer(bufnr) end) + it('re-highlights when markers return after resolution', function() + local bufnr = create_file_buffer({ + '<<<<<<< HEAD', + 'local x = 1', + '=======', + 'local x = 2', + '>>>>>>> feature', + }) + vim.api.nvim_set_current_buf(bufnr) + local cfg = default_config() + conflict.attach(bufnr, cfg) + + assert.is_true(#get_extmarks(bufnr) > 0) + + vim.api.nvim_win_set_cursor(0, { 2, 0 }) + conflict.resolve_ours(bufnr, cfg) + assert.are.equal(0, #get_extmarks(bufnr)) + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + '<<<<<<< HEAD', + 'local x = 1', + '=======', + 'local x = 2', + '>>>>>>> feature', + }) + vim.api.nvim_exec_autocmds('TextChanged', { buffer = bufnr }) + + assert.is_true(#get_extmarks(bufnr) > 0) + + conflict.detach(bufnr) + helpers.delete_buffer(bufnr) + end) + it('detaches after last conflict resolved', function() local bufnr = create_file_buffer({ '<<<<<<< HEAD',