Posted on :: Source Code git vim hack

tl;dr mc?^\([=><\|]\)\1\{6\}<CR>d?^<<<<<<<?0<CR>/^\([=><\|]\)\1\{6\}<CR>d/^>>>>>>>/0<CR>`c


Working with git, I found that I am missing very simple functionality from a few plugins I've checked out - accept the side 1 of the conflict region under cursor and remove the rest. Most plugins have actions to keep the ours side and the theirs side but, strangely, not the side I'm looking at right now.

Throughout this post I use the term side to refer to the area between any 2 adjacent git conflict markers, as that seems to be a historically reasonable name. I use zdiff3 exclusively where there is also a "third" side, but who cares. Let conflict region be the region of test between <<<<<<< and >>>>>>>. It is sometimes also called a hunk.

Solution

Here's the solution. No error handling, no conditions, pure vim.

:nnnoremap <leader>gc mc?^\([=><\|]\)\1\{6\}<CR>d?^<<<<<<<?0<CR>/^\([=><\|]\)\1\{6\}<CR>d/^>>>>>>>/0<CR>`c

In neovim/lua, I map it like this:

vim.keymap.set('n', '<leader>gc',
  [[mc?^\([=><\|]\)\1\{6\}<CR>d?^<<<<<<<?0<CR>/^\([=><\|]\)\1\{6\}<CR>d/^>>>>>>>/0<CR>`c]],
  { noremap = true }
)

Explanation

I have found ChatGPT suprisingly good at "understanding" and explaining this mapping, but I will break it down anyway, because, well, I like breaking things down.

First, find (up from the cursor) any conflict delimiter. That is

7 times =, >, < or |

or, in regex terms:

one of =, >, < or |, followed by 6 more of the same

In vim/neovim, special regex characters must be escaped, so this becomes a bit messy:

  ?^\([=><\|]\)\1\{6\}<CR>

Now we are one line above what we want to keep. Then, delete that line and everything above it until the beginning of the whole conflict region (<<<<<<<):

  ?^\([=><\|]\)\1\{6\}<CR>d?^<<<<<<<?0<CR>

?0 at the end of the search phrase (or /0 when searching forward) turns the search into a linewise motion which conveniently also removes the whole line containing the beginning marker when used as an operator to d.

Now a forward search to any conflict marker moves us to the line below the part we want to keep (as after the last deletion the cursor is on the first line of the conflict side of interest)...

  ?^\([=><\|]\)\1\{6\}<CR>d?^<<<<<<<?0<CR>/^\([=><\|]\)\1\{6\}<CR>

...followed by deletion until the final marker:

  ?^\([=><\|]\)\1\{6\}<CR>d?^<<<<<<<?0<CR>/^\([=><\|]\)\1\{6\}<CR>d/^>>>>>>>/0<CR>

And a cherry on top: remember the initial position of the cursor and revert it after the operation:

mc?^\([=><\|]\)\1\{6\}<CR>d?^<<<<<<<?0<CR>/^\([=><\|]\)\1\{6\}<CR>d/^>>>>>>>/0<CR>`c 

See a visualisation below:

             ?...            d?...          /...             d/...           `c
common text 1   common text 1
<<<<<<< ours   <<<<<<< ours   common text 1    common text 1   common text 1  common text 1
ours1          ours1         base1            base1           base1          base1
ours2          ours2          base2            base2           base2          base2
||||||| base  ||||||| base   base3`c          base3`c         base3`c        base3`c 
base1           base1          =======        =======        common text 2  common text 2
base2           base2          theirs          theirs
base3`c        base3`c        >>>>>>> theirs  >>>>>>> theirs
=======         =======        common text 2    common text 2
theirs          theirs
>>>>>>> theirs  >>>>>>> theirs
common text 2   common text 2

Legend:

  • - cursor position
  • `c - position of mark c
  • - lines scheduled to be deleted (placed on the left of each line)

Shortcomings

Admittedly, I have purposely kept this solution without any error handling. It does not work well when a side is empty, it is unclear what it should do when the cursor is on a conflict marker and it deletes stuff when the cursor is between hunks. I wish I could say I don't care. But I will try my best not to fix it to retain the crudity. It will be hard, it will be against my nature, but I will try.

The keymap has found its place in my neovim dotfiles. Feel free to bash me if it gets any less crude.

Other solutions

At first I thought it would be natural to first find the conflict side of interest between any conflict markers up and down from the cursor, yank it, then select the whole conflict region and substitute with the yanked content. But this makes it impossible to restore the original cursor position, as deleting lines with a mark inside deletes the mark. Note that in the presented solution the deletions before the mark conveniently preserve the logical position of the mark (decrease the line number associated with the mark) and deletions after the mark are irrelevant.

Table of Contents