diff --git a/doc/diffview.txt b/doc/diffview.txt index 5e0be7d1..fce711ee 100644 --- a/doc/diffview.txt +++ b/doc/diffview.txt @@ -133,6 +133,16 @@ COMMANDS *diffview-commands* • `dx`: Choose none of the versions of the conflict (delete the conflict region). + For operating on the merged file as a whole (useful when the merge + driver did not emit conflict markers, e.g., modify/delete conflicts + or jj with `merge-tool-edits-conflict-markers = false`): + • `cso`: Replace the merged buffer with the entire OURS + side. + • `cst`: Replace the merged buffer with the entire THEIRS + side. + • `csb`: Replace the merged buffer with the entire BASE + side. + For more info on these actions, see |diffview-actions-conflict_choose|. @@ -1717,6 +1727,22 @@ conflict_choose_all({version}) *diffview-actions-conflict_choos Parameters: ~ {version} `'ours'|'theirs'|'base'|'all'|'none'` + See |diffview-conflict-versions| to tell what versions of the + file these names correspond with. + +conflict_choose_side({version}) *diffview-actions-conflict_choose_side* + Contexts: `merge_tool`, `file_panel` + + Replace the entire merged buffer with the content of one whole side + (OURS, THEIRS, or BASE). Unlike |diffview-actions-conflict_choose_all|, + this does not parse conflict markers; it overwrites the buffer with + the side's lines verbatim. Useful when the merge driver did not emit + conflict markers (e.g., modify/delete conflicts, or jj with + `merge-tool-edits-conflict-markers = false`); accepting a side whose + path was empty effectively accepts the delete. + + Parameters: ~ + {version} `'ours'|'theirs'|'base'` See |diffview-conflict-versions| to tell what versions of the file these names correspond with. diff --git a/lua/diffview/actions.lua b/lua/diffview/actions.lua index a955a9d3..4d3da742 100644 --- a/lua/diffview/actions.lua +++ b/lua/diffview/actions.lua @@ -75,6 +75,7 @@ local pl = lazy.access(utils, "path") --[[@as PathLib ]] ---@field unstage_all fun() ---@field conflict_choose fun(target: DiffviewConflictTarget): fun() ---@field conflict_choose_all fun(target: DiffviewConflictTarget): AsyncFunc +---@field conflict_choose_side fun(target: DiffviewConflictSideTarget): AsyncFunc ---@field cycle_layout fun() ---@field diff_against_default_branch fun() ---@field diffget fun(target: DiffviewDiffgetTarget): fun() @@ -766,6 +767,55 @@ function M.conflict_choose(target) end, "merge_only") end +---Replace the entire MERGED buffer with the content of the OURS, THEIRS, or +---BASE side. Unlike `conflict_choose_all`, this does not parse conflict +---markers; it overwrites the whole buffer with the side's lines. Useful when +---the merge driver did not emit markers (e.g., modify/delete conflicts, or +---jj with `merge-tool-edits-conflict-markers = false`). +---@param target DiffviewConflictSideTarget +---@return AsyncFunc +function M.conflict_choose_side(target) + return tag( + async.void(function() + local view = lib.get_current_view() --[[@as DiffView ]] + + if not (view and view:instanceof(DiffView.__get())) then + return + end + ---@cast view DiffView + + if view.panel:is_focused() then + local item = view:infer_cur_file(false) ---@cast item -DirData + if not item then + return + end + + if not item.active then + await(view:set_file(item)) + end + end + + local main, bufnr = get_valid_main(view) + if not main then + return + end + ---@cast bufnr integer + + local src_bufnr = diff_copy_target(target) + if not (src_bufnr and api.nvim_buf_is_valid(src_bufnr)) then + return + end + + local src_lines = api.nvim_buf_get_lines(src_bufnr, 0, -1, false) + api.nvim_buf_set_lines(bufnr, 0, -1, false, src_lines) + + utils.set_cursor(main.id, 1, 0) + view.cur_layout:sync_scroll() + end), + "merge_only" + ) +end + ---@param target DiffviewDiffgetTarget ---@return fun() function M.diffget(target) diff --git a/lua/diffview/config.lua b/lua/diffview/config.lua index de82a21a..53202a26 100644 --- a/lua/diffview/config.lua +++ b/lua/diffview/config.lua @@ -50,6 +50,7 @@ end -- Targets consumed by action factories in `actions.lua` (referenced from keymaps). ---@alias DiffviewConflictTarget "ours"|"theirs"|"base"|"all"|"none" +---@alias DiffviewConflictSideTarget "ours"|"theirs"|"base" ---@alias DiffviewDiffgetTarget "ours"|"theirs"|"base"|"local" ---@class DiffviewKeymapOpts @@ -120,6 +121,9 @@ local conflict_keymaps = { { "n", "cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } }, { "n", "cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } }, { "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } }, + { "n", "cso", actions.conflict_choose_side("ours"), { desc = "Replace the MERGED buffer with the entire OURS side" } }, + { "n", "cst", actions.conflict_choose_side("theirs"), { desc = "Replace the MERGED buffer with the entire THEIRS side" } }, + { "n", "csb", actions.conflict_choose_side("base"), { desc = "Replace the MERGED buffer with the entire BASE side" } }, } ---@class DiffviewConfig diff --git a/lua/diffview/tests/functional/actions_spec.lua b/lua/diffview/tests/functional/actions_spec.lua index d9c66850..9bf292d7 100644 --- a/lua/diffview/tests/functional/actions_spec.lua +++ b/lua/diffview/tests/functional/actions_spec.lua @@ -279,6 +279,12 @@ describe("diffview.actions._is_applicable", function() assert.is_true(actions._is_applicable(fn, make_view({ merge_ctx = true }))) end) + it("propagates `merge_only` tag through `conflict_choose_side` factory", function() + local fn = actions.conflict_choose_side("ours") + assert.is_false(actions._is_applicable(fn, make_view())) + assert.is_true(actions._is_applicable(fn, make_view({ merge_ctx = true }))) + end) + it("returns false when view is nil and the action is `merge_only`", function() assert.is_false(actions._is_applicable(actions.next_conflict, nil)) end)