← Back to Index
DRAFT: This article is a work in progress

Vim annotate git conflicts markers

When solving git conflicts with git mergetool as explained in 2025-11-21-21-40-resolve-git-conflicts-easily, there are situations where I figured-out the conflit, but I still need to remove the conflict markers.

Turns out, vim has a native way to do so with :diffget REMOTE or :diffget BASE or :diffget LOCAL (see Vim: diff.txt).

But which is which ? When looking at a diff3 conflict, the conflict markers only mention HEAD, \(COMMID_ID or \)BRANCH_NAME:

class Square : public Rectangle {
public:
<<<<<<< HEAD
 static Rectangle* DefaultEmptyObject(); // (1)
||||||| 8134ad7
 //this comment is a bit outdated // (2)
 static Rectangle* DefaultEmpty();
=======
 //this comment is a bit outdated // (3)
 static const Rectangle* DefaultEmpty();
>>>>>>> new-branch
};

Imagine a world where vim would annotate conflict markers with the appropriate names:

class Square : public Rectangle {
public:
<<<<<<< HEAD (LOCAL/ours)
 static Rectangle* DefaultEmptyObject(); // (1)
||||||| 8134ad7 (BASE)
 //this comment is a bit outdated // (2)
 static Rectangle* DefaultEmpty();
======= (REMOTE/theirs)
 //this comment is a bit outdated // (3)
 static const Rectangle* DefaultEmpty();
>>>>>>> new-branch
};

Imagine no more! Here is a .vimrc snippet that make use of vim textprops to add those annotations:

" ---------------------------------------------------------------------------
" Git conflict marker virtual labels via text properties (vanilla Vim).
" ---------------------------------------------------------------------------
" ---- Highlight groups  --------------------
augroup GitConflictVirtTextHl
 autocmd!
 autocmd ColorScheme * call s:DefineHighlight()
augroup END
function! s:DefineHighlight() abort
 highlight default link GitConflictVirtTextLocal DiffText
 highlight default link GitConflictVirtTextBase DiffChange
 highlight default link GitConflictVirtTextSep DiffChange
 highlight default link GitConflictVirtTextRemote DiffText
endfunction
call s:DefineHighlight()
" ---- Property types --------------------------------------------------------
function! s:EnsurePropTypes() abort
 if exists('b:git_conflict_prop_types_defined') && b:git_conflict_prop_types_defined
 return
 endif
 " Define types (global) and ignore "already exists" errors.
 for [l:name, l:hl] in [
 \ ['GitConflictLocal', 'GitConflictVirtTextLocal'],
 \ ['GitConflictBase', 'GitConflictVirtTextBase'],
 \ ['GitConflictSep', 'GitConflictVirtTextSep'],
 \ ['GitConflictRemote', 'GitConflictVirtTextRemote'],
 \ ]
 try
 call prop_type_add(l:name, {'highlight': l:hl, 'combine': v:true})
 catch /^Vim\%((\a\+)\)\=:E969/ " E969: Property type already exists
 " fine
 endtry
 endfor
 let b:git_conflict_prop_types_defined = 1
endfunction
function! GitConflictClearVirtText() abort
 if !has('textprop') | return | endif
 call s:EnsurePropTypes()
 " Remove only our types.
 for t in ['GitConflictLocal', 'GitConflictBase', 'GitConflictSep', 'GitConflictRemote']
 call prop_remove({'type': t, 'all': 1})
 endfor
endfunction
function! GitConflictAnnotateVirtText() abort
 if !has('textprop') | return | endif
 call s:EnsurePropTypes()
 " Clean slate first, otherwise edits can leave stale labels behind.
 call GitConflictClearVirtText()
 let l:last = line('$')
 for lnum in range(1, l:last)
 let l:line = getline(lnum)
 if l:line =~# '^<<<<<<<\s\+'
 call s:AddVirtText(lnum, 'GitConflictLocal', ' (LOCAL/ours)')
 elseif l:line =~# '^=======$'
 call s:AddVirtText(lnum, 'GitConflictBase', ' (BASE)')
 elseif l:line =~# '^>>>>>>>\s\+'
 call s:AddVirtText(lnum, 'GitConflictRemote', ' (REMOTE/theirs)')
 endif
 endif
 endfor
endfunction
function! s:AddVirtText(lnum, type, text) abort
 call prop_add(a:lnum, 0, {
 \ 'type': a:type,
 \ 'text': a:text,
 \ 'text_align': 'after',
 \ })
endfunction
" ---- Auto-run only when conflict markers exist -----------------------------
function! s:MaybeAnnotate() abort
 " Fast pre-check: only run if any marker exists in a small scan window,
 " otherwise full-buffer scanning on every TextChanged can be costly.
 " If you prefer, remove this and always annotate.
 let l:max = min([line('$'), 500])
 for lnum in range(1, l:max)
 let l = getline(lnum)
 if l =~# '^<<<<<<<\s\+' || l =~# '^>>>>>>>\s\+' || l =~# '^=======$' || l =~# '^|||||||\s\+'
 call GitConflictAnnotateVirtText()
 return
 endif
 endfor
 " No markers found (at least early in file): clear any stale props.
 call GitConflictClearVirtText()
endfunction
augroup GitConflictVirtText
 autocmd!
 " Run on load and when entering the buffer/window.
 autocmd BufReadPost,BufWinEnter * call s:MaybeAnnotate()
 " Update after edits. (You can remove TextChangedI if you prefer.)
 " autocmd TextChanged,TextChangedI * call s:MaybeAnnotate()
augroup END
" Optional user commands.
command! GitConflictVirtTextOn call GitConflictAnnotateVirtText()
command! GitConflictVirtTextOff call GitConflictClearVirtText()