Skip to content

Commit 2267daa

Browse files
authored
feat(dx): add qol improvements to diffconflicts (#2)
- Auto advance on conflict resolution - Auto close diffconflicts when all conflicts are resolved fix jj conflict detection
1 parent e5393fc commit 2267daa

2 files changed

Lines changed: 147 additions & 6 deletions

File tree

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ For example, with [Lazy](https://github.com/folke/lazy.nvim):
5050
-- set to nil to disable the command
5151
with_history = "DiffConflictsWithHistory",
5252
},
53+
-- Quality-of-life options
54+
qol = {
55+
-- After saving (:w), automatically close the diff view and jump to the next
56+
-- conflict in the file (if any).
57+
advance_on_save = true,
58+
-- If no conflicts remain after saving, quit Neovim (:qa). This is useful
59+
-- when running from `git mergetool` / `jj resolve`.
60+
quit_on_done = true,
61+
},
5362
}
5463
}
5564
```
@@ -63,7 +72,8 @@ git config --global mergetool.diffconflicts.trustExitCode true
6372
git config --global mergetool.keepBackup false
6473
```
6574

66-
Configure Jujutsu to use this plugin as a merge tool (requires the default `"diff"` conflict marker style):
75+
Configure Jujutsu to use this plugin as a merge tool
76+
(requires the default `"diff"` conflict marker style):
6777

6878
```toml
6979
[merge-tools.diffconflicts]
@@ -105,6 +115,8 @@ the right side shows the differences between the branches.
105115

106116
So all you need to do is edit the left side to resolve the conflicts.
107117

118+
By default, saving the file (`:w`) will automatically advance to the next conflict in the file, and if there are no conflicts left it will quit Neovim (so your merge tool can continue). You can customize this behavior via `qol.advance_on_save` and `qol.quit_on_done`.
119+
108120
To abort the merge, simply `:cquit`.
109121

110122
### Lua API

lua/diffconflicts/init.lua

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ local config = {
77
show_history = "DiffConflictsShowHistory",
88
with_history = "DiffConflictsWithHistory",
99
},
10+
qol = {
11+
advance_on_save = true,
12+
quit_on_done = true,
13+
},
1014
}
1115

16+
local advance_augroup = vim.api.nvim_create_augroup("diffconflicts.nvim.advance", { clear = false })
17+
1218
local function is_jj_repo()
1319
local buf_path = vim.api.nvim_buf_get_name(0)
1420
local start = buf_path ~= "" and vim.fn.fnamemodify(buf_path, ":p:h") or vim.uv.cwd()
@@ -204,8 +210,13 @@ local function jj_setup_diff_splits(conflicts)
204210
local conflicted_content = vim.api.nvim_buf_get_lines(0, 0, -1, false)
205211
local original_filetype = vim.bo.filetype
206212

213+
local left_buf = vim.api.nvim_get_current_buf()
214+
local left_win = vim.api.nvim_get_current_win()
215+
207216
vim.cmd.vsplit({ mods = { split = "belowright" } })
208217
vim.cmd.enew()
218+
local right_win = vim.api.nvim_get_current_win()
219+
local right_buf = vim.api.nvim_get_current_buf()
209220
local right_side = jj_get_content_for_side("right_side", conflicts, vim.deepcopy(conflicted_content))
210221
vim.api.nvim_buf_set_lines(0, 0, -1, false, right_side)
211222
vim.cmd.file("snapshot")
@@ -214,12 +225,48 @@ local function jj_setup_diff_splits(conflicts)
214225
vim.cmd.diffthis()
215226

216227
vim.cmd.wincmd("p")
228+
-- Ensure we are back on the original buffer/window.
229+
if vim.api.nvim_get_current_buf() ~= left_buf and vim.api.nvim_win_is_valid(left_win) then
230+
vim.api.nvim_set_current_win(left_win)
231+
end
217232
local left_side = jj_get_content_for_side("left_side", conflicts, vim.deepcopy(conflicted_content))
218233
vim.api.nvim_buf_set_lines(0, 0, -1, false, left_side)
219234
vim.cmd.diffthis()
220235

221236
vim.cmd.diffupdate()
222237
vim.fn.cursor(conflicts[1].top_line, 1)
238+
239+
-- QoL: save-to-advance (and optionally quit when done).
240+
if config.qol and config.qol.advance_on_save then
241+
vim.api.nvim_clear_autocmds({ group = advance_augroup, buffer = left_buf })
242+
vim.api.nvim_create_autocmd("BufWritePost", {
243+
group = advance_augroup,
244+
buffer = left_buf,
245+
callback = function()
246+
-- Close the snapshot window/buffer if still around.
247+
if vim.api.nvim_win_is_valid(right_win) then
248+
pcall(vim.api.nvim_win_close, right_win, true)
249+
end
250+
if vim.api.nvim_buf_is_valid(right_buf) then
251+
pcall(vim.api.nvim_buf_delete, right_buf, { force = true })
252+
end
253+
254+
-- If there are more conflicts, reopen diff view; otherwise optionally quit.
255+
local lines = vim.api.nvim_buf_get_lines(left_buf, 0, -1, false)
256+
if buffer_looks_like_jj_conflict(lines) then
257+
-- Re-run using inferred marker length so we always match the buffer.
258+
jj_run(false, detect_jj_marker_length_from_buffer(lines), nil)
259+
return
260+
end
261+
262+
if config.qol and config.qol.quit_on_done then
263+
-- If we were launched as a mergetool, leaving Neovim is the smoothest way
264+
-- to hand control back to the VCS tooling.
265+
pcall(vim.cmd, "qa")
266+
end
267+
end,
268+
})
269+
end
223270
end
224271

225272
local function jj_setup_history_view(base_path, left_path, right_path)
@@ -324,14 +371,65 @@ local function has_conflicts()
324371
return conflict_count > 0
325372
end
326373

374+
local function detect_jj_marker_length_from_buffer(lines)
375+
-- jj conflict markers look like:
376+
-- <<<<<<< <description>
377+
-- %%%%%%% <description>
378+
-- +++++++ <description>
379+
-- >>>>>>> <description>
380+
--
381+
-- The marker length is configurable; infer it from the first "<<<<<<<" line.
382+
for _, line in ipairs(lines) do
383+
local run = line:match("^(<+)%s.+$")
384+
if run then
385+
return #run
386+
end
387+
end
388+
return nil
389+
end
390+
391+
local function buffer_looks_like_jj_conflict(lines)
392+
local has_top = false
393+
local has_diff = false
394+
local has_snapshot = false
395+
local has_bottom = false
396+
397+
for _, line in ipairs(lines) do
398+
if not has_top and line:match("^<+%s.+$") then
399+
has_top = true
400+
elseif not has_diff and line:match("^%%+%%+%s.+$") then
401+
-- "%%%%%%%" in Lua patterns needs escaping; this matches 2+ '%' chars then space.
402+
has_diff = true
403+
elseif not has_snapshot and line:match("^%+%+%s.+$") then
404+
-- "+++++++" (2+ '+' chars then space)
405+
has_snapshot = true
406+
elseif not has_bottom and line:match("^>+%s.+$") then
407+
has_bottom = true
408+
end
409+
end
410+
411+
return has_top and has_bottom and (has_diff or has_snapshot)
412+
end
413+
327414
local function diff_confl()
328415
if effective_vcs() == "jj" then
329416
jj_run(false, nil, nil)
330417
return
331418
end
332419

420+
-- If we were invoked on a jj-style conflict buffer outside of a jj repo
421+
-- (e.g. `jj resolve` temp paths), fall back to jj parsing anyway.
422+
do
423+
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
424+
if buffer_looks_like_jj_conflict(lines) then
425+
jj_run(false, detect_jj_marker_length_from_buffer(lines), nil)
426+
return
427+
end
428+
end
429+
333430
local orig_buf = vim.api.nvim_get_current_buf()
334431
local orig_ft = vim.bo.filetype
432+
local left_win = vim.api.nvim_get_current_win()
335433

336434
local conflict_style
337435
if config.vcs == "git" then
@@ -344,14 +442,16 @@ local function diff_confl()
344442
-- Set up the right-hand side.
345443
vim.cmd("rightb vsplit")
346444
vim.cmd("enew")
445+
local right_win = vim.api.nvim_get_current_win()
446+
local right_buf = vim.api.nvim_get_current_buf()
347447
vim.cmd('silent execute "read #" .. ' .. orig_buf)
348448
vim.api.nvim_buf_set_lines(0, 0, 1, false, {}) -- Delete the first line
349449
vim.cmd("silent file RCONFL")
350450
vim.bo.filetype = orig_ft
351451
vim.cmd("diffthis")
352452

353-
vim.cmd("silent g/^<<<<<<< /,/^=======\\r\\?$/d")
354-
vim.cmd("silent g/^>>>>>>> /d")
453+
vim.cmd("silent! g/^<<<<<<< /,/^=======\\r\\?$/d")
454+
vim.cmd("silent! g/^>>>>>>> /d")
355455

356456
vim.bo.modifiable = false
357457
vim.bo.readonly = true
@@ -364,13 +464,42 @@ local function diff_confl()
364464
vim.cmd("diffthis")
365465

366466
if conflict_style:lower() == "diff3" or conflict_style:lower() == "zdiff3" then
367-
vim.cmd("silent g/^||||||| \\?/,/^>>>>>>> /d")
467+
vim.cmd("silent! g/^||||||| \\?/,/^>>>>>>> /d")
368468
else
369-
vim.cmd("silent g/^=======\\r\\?$/,/^>>>>>>> /d")
469+
vim.cmd("silent! g/^=======\\r\\?$/,/^>>>>>>> /d")
370470
end
371-
vim.cmd("silent g/^<<<<<<< /d")
471+
vim.cmd("silent! g/^<<<<<<< /d")
372472

373473
vim.cmd("diffupdate")
474+
475+
-- QoL: save-to-advance (and optionally quit when done).
476+
if config.qol and config.qol.advance_on_save then
477+
vim.api.nvim_clear_autocmds({ group = advance_augroup, buffer = orig_buf })
478+
vim.api.nvim_create_autocmd("BufWritePost", {
479+
group = advance_augroup,
480+
buffer = orig_buf,
481+
callback = function()
482+
if vim.api.nvim_win_is_valid(right_win) then
483+
pcall(vim.api.nvim_win_close, right_win, true)
484+
end
485+
if vim.api.nvim_buf_is_valid(right_buf) then
486+
pcall(vim.api.nvim_buf_delete, right_buf, { force = true })
487+
end
488+
if vim.api.nvim_win_is_valid(left_win) then
489+
pcall(vim.api.nvim_set_current_win, left_win)
490+
end
491+
492+
if has_conflicts() then
493+
diff_confl()
494+
return
495+
end
496+
497+
if config.qol and config.qol.quit_on_done then
498+
pcall(vim.cmd, "qa")
499+
end
500+
end,
501+
})
502+
end
374503
end
375504

376505
local function show_history()

0 commit comments

Comments
 (0)