Skip to content

Instantly share code, notes, and snippets.

@jacobobryant
Created June 2, 2026 17:26
Show Gist options
  • Select an option

  • Save jacobobryant/b03c07d9d9ce58a3ca3e864514191c73 to your computer and use it in GitHub Desktop.

Select an option

Save jacobobryant/b03c07d9d9ce58a3ca3e864514191c73 to your computer and use it in GitHub Desktop.
show additions/deletions in vim-fugitive

The diff was applied to 3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0. After applying, the :Git window looks like (green/red coloring):

Head: my-branch
Help: g?

Untracked (1)
? file_1.txt

Unstaged (4)
M file_2.txt     +16  -0
M file_3.txt      +8  -1
M file_4.txt      +6  -1
M file_5.txt    +403 -23
diff --git a/autoload/fugitive.vim b/autoload/fugitive.vim
index d0748c1..e807e36 100644
--- a/autoload/fugitive.vim
+++ b/autoload/fugitive.vim
@@ -2592,6 +2592,10 @@ function! s:FormatFile(dict) abort
return a:dict.status . ' ' . a:dict.filename
endfunction
+function! s:StripDiffStats(text) abort
+ return substitute(a:text, ' \++\d\+ \+-\d\+$', '', '')
+endfunction
+
function! s:Format(val) abort
if type(a:val) == type({})
return s:Format{a:val.type}(a:val)
@@ -2617,6 +2621,39 @@ function! s:AddSection(to, label, lines, ...) abort
call extend(a:to.lines, ['', a:label . (len(note) ? ': ' . note : ' (' . len(a:lines) . ')')] + s:Format(a:lines))
endfunction
+function! s:DiffStats(diff_section, files) abort
+ call fugitive#Wait(a:diff_section)
+ let headers = {}
+ let stats = []
+ let i = 0
+ while i < len(a:files)
+ let file = a:files[i]
+ if file.status ==# 'U'
+ let header = 'diff --cc ' . s:Quote(file.relative[0])
+ else
+ let header = 'diff --git ' . s:Quote(file.relative[-1]) . ' ' . s:Quote(file.relative[0])
+ endif
+ let headers[header] = i
+ call add(stats, [0, 0])
+ let i += 1
+ endwhile
+ let current = -1
+ for line in a:diff_section.stdout
+ if line =~# '^diff '
+ let current = get(headers, line, -1)
+ elseif current < 0
+ continue
+ elseif line =~# '^+++ \|^--- '
+ continue
+ elseif line[0:0] ==# '+'
+ let stats[current][0] += 1
+ elseif line[0:0] ==# '-'
+ let stats[current][1] += 1
+ endif
+ endfor
+ return stats
+endfunction
+
function! s:AddDiffSection(to, stat, label, files) abort
if empty(a:files)
return
@@ -2625,8 +2662,20 @@ function! s:AddDiffSection(to, stat, label, files) abort
let expanded = a:stat.expanded[a:label]
let was_expanded = get(getbufvar(a:stat.bufnr, 'fugitive_expanded', {}), a:label, {})
call extend(a:to.lines, ['', a:label . ' (' . len(a:files) . ')'])
+ let stats = s:DiffStats(diff_section, a:files)
+ let formatted = map(copy(a:files), 's:Format(v:val)')
+ let max_name = max(map(copy(formatted), 'len(v:val)') + [0])
+ let max_adds = 1
+ let max_dels = 1
+ for [adds, dels] in stats
+ let max_adds = max([max_adds, len(string(adds))])
+ let max_dels = max([max_dels, len(string(dels))])
+ endfor
+ let i = 0
for file in a:files
- call add(a:to.lines, s:Format(file))
+ let suffix = printf('%*s %*s', max_adds + 1, '+' . stats[i][0], max_dels + 1, '-' . stats[i][1])
+ let line = formatted[i] . repeat(' ', max_name - len(formatted[i]) + 2) . suffix
+ call add(a:to.lines, line)
if has_key(was_expanded, file.filename)
let [diff, start] = s:StageInlineGetDiff(diff_section, file)
if len(diff)
@@ -2634,6 +2683,7 @@ function! s:AddDiffSection(to, stat, label, files) abort
call extend(a:to.lines, diff)
endif
endif
+ let i += 1
endfor
endfunction
@@ -4275,7 +4325,7 @@ function! s:StageSeek(info, fallback) abort
endif
let i = 0
while len(getline(line))
- let filename = matchstr(getline(line), '^[A-Z?] \zs.*')
+ let filename = s:StripDiffStats(matchstr(getline(line), '^[A-Z?] \zs.*'))
if len(filename) &&
\ ((info.filename[-1:-1] ==# '/' && filename[0 : len(info.filename) - 1] ==# info.filename) ||
\ (filename[-1:-1] ==# '/' && filename ==# info.filename[0 : len(filename) - 1]) ||
@@ -4496,7 +4546,7 @@ function! s:StageInfo(...) abort
let index += 1
endif
endwhile
- let text = matchstr(getline(lnum), '^[A-Z?] \zs.*')
+ let text = s:StripDiffStats(matchstr(getline(lnum), '^[A-Z?] \zs.*'))
let file = s:StatusSectionFile(heading, text)
let relative = get(file, 'relative', len(text) ? [text] : [])
return {'section': matchstr(heading, '^\u\l\+'),
@@ -4578,7 +4628,7 @@ function! s:Selection(arg1, ...) abort
endif
let results[-1].lnum = lnum
elseif line =~# '^[A-Z?] '
- let text = matchstr(line, '^[A-Z?] \zs.*')
+ let text = s:StripDiffStats(matchstr(line, '^[A-Z?] \zs.*'))
let file = s:StatusSectionFile(template.heading, text)
let relative = get(file, 'relative', len(text) ? [text] : [])
call add(results, extend(deepcopy(template), {
diff --git a/syntax/fugitive.vim b/syntax/fugitive.vim
index c7fe201..6035093 100644
--- a/syntax/fugitive.vim
+++ b/syntax/fugitive.vim
@@ -32,6 +32,8 @@ syn region fugitiveHunk start=/^\%(@@\+ -\)\@=/ end=/^\%([A-Za-z?@]\|$\)\@=/ con
for s:section in ['Untracked', 'Unstaged', 'Staged']
exe 'syn region fugitive' . s:section . 'Section start=/^\%(' . s:section . ' .*(\d\++\=)$\)\@=/ contains=fugitive' . s:section . 'Heading end=/^$/ fold'
exe 'syn match fugitive' . s:section . 'Modifier /^[MADRCU?] / contained containedin=fugitive' . s:section . 'Section'
+ exe 'syn match fugitiveAdded /+\d\+\ze \+-\d\+$/ contained containedin=fugitive' . s:section . 'Section'
+ exe 'syn match fugitiveRemoved /-\d\+$/ contained containedin=fugitive' . s:section . 'Section'
exe 'syn cluster fugitiveSection add=fugitive' . s:section . 'Section'
exe 'syn match fugitive' . s:section . 'Heading /^[A-Z][a-z][^:]*\ze (\d\++\=)$/ contains=fugitivePreposition contained nextgroup=fugitiveCount skipwhite'
endfor
@@ -53,5 +55,7 @@ hi def link fugitiveStop Function
hi def link fugitiveHash Identifier
hi def link fugitiveSymbolicRef Function
hi def link fugitiveCount Number
+hi def fugitiveAdded ctermfg=DarkGreen guifg=#22863a
+hi def fugitiveRemoved ctermfg=DarkRed guifg=#cb2431
let b:current_syntax = "fugitive"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment