" This is the minimum number of characters required between regions of change " in a line. It's somewhat arbitrary: higher values mean less visual busyness; " lower values mean more detail. let s:gap_between_regions = 5 " Calculates the changed portions of lines. " " Based on: " " - diff-highlight (included with git) " https://github.com/git/git/blob/master/contrib/diff-highlight/DiffHighlight.pm " " - Diff Strategies, Neil Fraser " https://neil.fraser.name/writing/diff/ " Returns a list of intra-line changed regions. " Each element is a list: " " [ " line number (1-based), " type ('+' or '-'), " start column (1-based, inclusive), " stop column (1-based, inclusive), " ] " " Args: " hunk_body - list of lines function! gitgutter#diff_highlight#process(hunk_body) " Check whether we have the same number of lines added as removed. let [removed, added] = [0, 0] for line in a:hunk_body if line[0] == '-' let removed += 1 elseif line[0] == '+' let added += 1 endif endfor if removed != added return [] endif let regions = [] for i in range(removed) " pair lines by position let rline = a:hunk_body[i] let aline = a:hunk_body[i + removed] call s:diff(rline, aline, i, i+removed, 0, 0, regions, 1) endfor return regions endfunction function! s:diff(rline, aline, rlinenr, alinenr, rprefix, aprefix, regions, whole_line) " diff marker does not count as a difference in prefix let start = a:whole_line ? 1 : 0 let prefix = s:common_prefix(a:rline[start:], a:aline[start:]) if a:whole_line let prefix += 1 endif let [rsuffix, asuffix] = s:common_suffix(a:rline, a:aline, prefix+1) " region of change (common prefix and suffix removed) let rtext = a:rline[prefix+1:rsuffix-1] let atext = a:aline[prefix+1:asuffix-1] " singular insertion if empty(rtext) if !a:whole_line || len(atext) != len(a:aline) " not whole line call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1, a:aprefix+asuffix+1-1]) endif return endif " singular deletion if empty(atext) if !a:whole_line || len(rtext) != len(a:rline) " not whole line call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1, a:rprefix+rsuffix+1-1]) endif return endif " two insertions let j = stridx(atext, rtext) if j != -1 call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1, a:aprefix+prefix+j+1]) call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1+j+len(rtext), a:aprefix+asuffix+1-1]) return endif " two deletions let j = stridx(rtext, atext) if j != -1 call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1, a:rprefix+prefix+j+1]) call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1+j+len(atext), a:rprefix+rsuffix+1-1]) return endif " two edits let lcs = s:lcs(rtext, atext) " TODO do we need to ensure we don't get more than 2 elements when splitting? if len(lcs) > s:gap_between_regions let redits = s:split(rtext, lcs) let aedits = s:split(atext, lcs) call s:diff(redits[0], aedits[0], a:rlinenr, a:alinenr, a:rprefix+prefix+1, a:aprefix+prefix+1, a:regions, 0) call s:diff(redits[1], aedits[1], a:rlinenr, a:alinenr, a:rprefix+prefix+1+len(redits[0])+len(lcs), a:aprefix+prefix+1+len(aedits[0])+len(lcs), a:regions, 0) return endif " fall back to highlighting entire changed area " if a change (but not the whole line) if !a:whole_line || ((prefix != 0 || rsuffix != len(a:rline)) && prefix+1 < rsuffix) call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1, a:rprefix+rsuffix+1-1]) endif " if a change (but not the whole line) if !a:whole_line || ((prefix != 0 || asuffix != len(a:aline)) && prefix+1 < asuffix) call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1, a:aprefix+asuffix+1-1]) endif endfunction function! s:lcs(s1, s2) if empty(a:s1) || empty(a:s2) return '' endif let matrix = map(repeat([repeat([0], len(a:s2)+1)], len(a:s1)+1), 'copy(v:val)') let maxlength = 0 let endindex = len(a:s1) for i in range(1, len(a:s1)) for j in range(1, len(a:s2)) if a:s1[i-1] ==# a:s2[j-1] let matrix[i][j] = 1 + matrix[i-1][j-1] if matrix[i][j] > maxlength let maxlength = matrix[i][j] let endindex = i - 1 endif endif endfor endfor return a:s1[endindex - maxlength + 1 : endindex] endfunction if $VIM_GITGUTTER_TEST function! gitgutter#diff_highlight#lcs(s1, s2) return s:lcs(a:s1, a:s2) endfunction endif " Returns 0-based index of last character of common prefix " If there is no common prefix, returns -1. " " a, b - strings " function! s:common_prefix(a, b) let len = min([len(a:a), len(a:b)]) if len == 0 return -1 endif for i in range(len) if a:a[i:i] != a:b[i:i] return i - 1 endif endfor return i endfunction if $VIM_GITGUTTER_TEST function! gitgutter#diff_highlight#common_prefix(a, b) return s:common_prefix(a:a, a:b) endfunction endif " Returns 0-based indices of start of common suffix " " a, b - strings " start - 0-based index to start from function! s:common_suffix(a, b, start) let [sa, sb] = [len(a:a), len(a:b)] while sa >= a:start && sb >= a:start if a:a[sa] ==# a:b[sb] let sa -= 1 let sb -= 1 else break endif endwhile return [sa+1, sb+1] endfunction if $VIM_GITGUTTER_TEST function! gitgutter#diff_highlight#common_suffix(a, b, start) return s:common_suffix(a:a, a:b, a:start) endfunction endif " Split a string on another string. " Assumes 1 occurrence of the delimiter. function! s:split(str, delimiter) let i = stridx(a:str, a:delimiter) if i == 0 return ['', a:str[len(a:delimiter):]] endif return [a:str[:i-1], a:str[i+len(a:delimiter):]] endfunction if $VIM_GITGUTTER_TEST function! gitgutter#diff_highlight#split(str, delimiter) return s:split(a:str, a:delimiter) endfunction endif