Skip to content

Commit 8dff975

Browse files
committed
feat: sketch implementation of calculate_porcelain()
this uses `git diff --word-diff=porcelain --unified=0` but is far from finished. This commits preliminary ideas and results.
1 parent 81daf9c commit 8dff975

File tree

1 file changed

+119
-10
lines changed

1 file changed

+119
-10
lines changed

autoload/lightline/gitdiff.vim

Lines changed: 119 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,32 @@ function! lightline#gitdiff#write_to_cache(soft) abort
1212
return
1313
endif
1414

15-
let g:lightline#gitdiff#cache[bufnr('%')] = s:calculate()
15+
" let g:lightline#gitdiff#cache[bufnr('%')] = s:calculate_numstat()
16+
let g:lightline#gitdiff#cache[bufnr('%')] = s:calculate_porcelain()
1617
endfunction
1718

1819
" format returns how many lines were added and/or deleted in a nicely
1920
" formatted string. The output can be configured with global variables.
2021
function! lightline#gitdiff#format(data) abort
2122
let l:indicator_added = get(g:, 'lightline#gitdiff#indicator_added', 'A: ')
2223
let l:indicator_deleted = get(g:, 'lightline#gitdiff#indicator_deleted', 'D: ')
24+
let l:indicator_modified = get(g:, 'lightline#gitdiff#indicator_modified', 'M: ')
2325
let l:separator = get(g:, 'lightline#gitdiff#separator', ' ')
2426

2527
let l:lines_added = has_key(a:data, 'A') ? l:indicator_added . a:data['A'] : ''
2628
let l:lines_deleted = has_key(a:data, 'D') ? l:indicator_deleted . a:data['D'] : ''
29+
let l:lines_modified = has_key(a:data, 'M') ? l:indicator_modified . a:data['M'] : ''
2730

28-
return join([l:lines_added, l:lines_deleted], l:separator)
31+
return join([l:lines_added, l:lines_deleted, l:lines_modified], l:separator)
2932
endfunction
3033

31-
" calculate queries git to get the amount of lines that were added and/or
34+
" calculate_numstat queries git to get the amount of lines that were added and/or
3235
" deleted. It returns a dict with two keys: 'A' and 'D'. 'A' holds how many
3336
" lines were added, 'D' holds how many lines were deleted.
3437
"
3538
" This function is the most expensive one. It calls git twice: to check
3639
" whether the buffer is in a git repository and to do the actual calculation.
37-
function! s:calculate() abort
40+
function! s:calculate_numstat() abort
3841
if !executable('git') || !s:is_inside_work_tree()
3942
" b/c cannot do anything
4043
return {}
@@ -63,19 +66,125 @@ function! s:calculate() abort
6366
return l:ret
6467
endfunction
6568

69+
function! s:calculate_porcelain() abort
70+
let l:indicator_groups = s:transcode_diff_porcelain(s:get_diff_porcelain())
71+
72+
let l:indicators = map(copy(l:indicator_groups), { idx, val -> s:parse_indicator_group(val) })
73+
74+
let l:lines_added = len(filter(copy(l:indicators), { idx, val -> val ==# 'A' }))
75+
let l:lines_deleted = len(filter(copy(l:indicators), { idx, val -> val ==# 'D' }))
76+
let l:lines_modified = len(filter(copy(l:indicators), { idx, val -> val ==# 'M' }))
77+
78+
let l:ret = {}
79+
80+
if l:lines_added > 0
81+
let l:ret['A'] = l:lines_added
82+
endif
83+
84+
if l:lines_deleted > 0
85+
let l:ret['D'] = l:lines_deleted
86+
endif
87+
88+
if l:lines_modified > 0
89+
let l:ret['M'] = l:lines_modified
90+
endif
91+
92+
return l:ret
93+
endfunction
94+
95+
" get_diff_porcelain returns the output of git's word-diff as list. The header
96+
" of the diff is removed b/c it is not needed.
6697
function! s:get_diff_porcelain() abort
6798
" return the ouput of `git diff --word-diff=porcelain --unified=0` linewise
6899
"
69-
" systemlist()
100+
let l:porcelain = systemlist('cd ' . expand('%:p:h:S') . ' && git diff --word-diff=porcelain --unified=0 -- ' . expand('%:t:S'))
101+
return l:porcelain[4:]
102+
endfunction
103+
104+
" transcode_diff_porcelain turns a diff porcelain into a list of lists such as
105+
" the following:
106+
"
107+
" [ [' ', '-', '~'], ['~'], ['+', '~'], ['+', '-', '~' ] ]
108+
"
109+
" This translates to Deletion, Addition, Addition and Modification. The
110+
" characters ' ', '-', '+', '~' are the very first columns of a
111+
" `--word-diff=porcelain` output and include everything we need for
112+
" calculation.
113+
function! s:transcode_diff_porcelain(porcelain) abort
114+
" b/c we do not need the line identifiers
115+
call filter(a:porcelain, { idx, val -> val !~# '^@@' })
116+
117+
" b/c we only need the identifiers at the first char of each diff line
118+
call map(a:porcelain, { idx, val -> strcharpart(val, -1, 2) })
119+
120+
return s:group_at_right({ el -> el ==# '~' }, a:porcelain, v:true)
121+
endfunction
122+
123+
" parse_indicator_group parses a group of indicators af a word-diff porcelain
124+
" that describes an Addition, Delition or Modification. It returns a single
125+
" character of either 'A', 'D', 'M' for the type of diff that is recorded by
126+
" the group.
127+
function! s:parse_indicator_group(indicators) abort
128+
let l:action = '' " A_ddition, D_eletion or M_odification
129+
let l:changer = ''
130+
131+
for el in a:indicators
132+
if el ==# ' ' && l:changer ==# ''
133+
" b/c first element with no meaning
134+
continue
135+
endif
136+
137+
if el ==# '+' || el ==# '-'
138+
if l:changer ==# ''
139+
" changer was found
140+
let l:changer = el
141+
continue
142+
else
143+
" 2nd changer found i.e., modification
144+
return 'M'
145+
endif
146+
endif
147+
148+
if el ==# '~'
149+
if l:changer ==# '' || l:changer ==# '+'
150+
" a single `~` stands for a new line i.e., an addition
151+
return 'A'
152+
else
153+
" b/c changer must be '-'
154+
return 'D'
155+
endif
156+
endif
157+
endfor
158+
159+
" b/c we should never end up here
160+
echoerr 'Could not parse indicator group of word-diff porcelain'
70161
endfunction
71162

72-
function! s:clean_diff_porcelain() abort
73-
" return cleaned porcelain (remove header of diff, only lines between `@@`)
163+
" group_at groups a list of elements where `f` evaluates to true returning a
164+
" list of lists. `f` must take a single parameter; each element is used as an
165+
" argument to `f`. If `borders` is true, the matched element is included in
166+
" each group at the beginning.
167+
function! s:group_at(f, list, borders) abort
168+
let grouped_list = []
169+
170+
for el in a:list
171+
if a:f(el)
172+
call add(l:grouped_list, [])
173+
if !a:borders
174+
continue
175+
endif
176+
endif
177+
178+
call add(l:grouped_list[len(l:grouped_list)], el)
179+
endfor
180+
181+
return grouped_list
74182
endfunction
75183

76-
function! s:group_diff_porcelain() abort
77-
" return grouped porcelain by `@@` -> each group is a single change
78-
" in `calculate()` each group is analysed and a final result calculated
184+
" group_at_right behaves the same as group_at except that the matched element
185+
" is included in each group /at the end/.
186+
function! s:group_at_right(f, list, borders) abort
187+
return reverse(s:group_at(a:f, reverse(a:list), a:borders))
79188
endfunction
80189

81190
function! s:is_inside_work_tree() abort "{{{

0 commit comments

Comments
 (0)