diff --git a/autoload/gitgutter/diff_highlight.vim b/autoload/gitgutter/diff_highlight.vim index 3031582..4851058 100644 --- a/autoload/gitgutter/diff_highlight.vim +++ b/autoload/gitgutter/diff_highlight.vim @@ -1,3 +1,9 @@ +" 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: @@ -42,70 +48,121 @@ function! gitgutter#diff_highlight#process(hunk_body) let rline = a:hunk_body[i] let aline = a:hunk_body[i + removed] - let prefix = s:common_prefix(rline, aline) - let [rsuffix, asuffix] = s:common_suffix(rline, aline, prefix+1) - - let rtext = rline[prefix+1:rsuffix-1] - let atext = aline[prefix+1:asuffix-1] - - " singular insertion - if empty(rtext) - if len(atext) != len(aline) " not whole line - call add(regions, [i+1+removed, '+', prefix+1+1, asuffix+1-1]) - endif - continue - endif - - " singular deletion - if empty(atext) - if len(rtext) != len(rline) " not whole line - call add(regions, [i+1, '-', prefix+1+1, rsuffix+1-1]) - endif - continue - endif - - " two insertions - let j = stridx(atext, rtext) - if j != -1 - call add(regions, [i+1+removed, '+', prefix+1+1, prefix+j+1]) - call add(regions, [i+1+removed, '+', prefix+1+1+j+len(rtext), asuffix+1-1]) - continue - endif - - " two deletions - let j = stridx(rtext, atext) - if j != -1 - call add(regions, [i+1, '-', prefix+1+1, prefix+j+1]) - call add(regions, [i+1, '-', prefix+1+1+j+len(atext), rsuffix+1-1]) - continue - endif - - " fall back to highlighting entire changed area - - " if a change (but not the whole line) - if (prefix != 0 || rsuffix != len(rline)) && prefix+1 < rsuffix - call add(regions, [i+1, '-', prefix+1+1, rsuffix+1-1]) - endif - - " if a change (but not the whole line) - if (prefix != 0 || asuffix != len(aline)) && prefix+1 < asuffix - call add(regions, [i+1+removed, '+', prefix+1+1, asuffix+1-1]) - endif + 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) + + 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) + if len(lcs) > s:gap_between_regions + let redits = split(rtext, lcs) + let aedits = split(atext, lcs) + call s:diff(redits[0], aedits[0], a:rlinenr, a:alinenr, prefix+1, prefix+1, a:regions, 0) + call s:diff(redits[1], aedits[1], a:rlinenr, a:alinenr, prefix+1+len(redits[0])+len(lcs), 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] ==# a:s2[j] + let matrix[i][j] = 1 + matrix[i-1][j-1] + if matrix[i][j] > maxlength + let maxlength = matrix[i][j] + let endindex = i + 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 -" Does not treat leading +/- as different. +" 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)]) - " ignore initial +/- - for i in range(1, len - 1) + for i in range(len) if a:a[i:i] != a:b[i:i] return i - 1 endif diff --git a/test/test_gitgutter.vim b/test/test_gitgutter.vim index c3c1d02..5dd313f 100644 --- a/test/test_gitgutter.vim +++ b/test/test_gitgutter.vim @@ -917,14 +917,22 @@ endfunction function Test_common_prefix() " nothing in common - call assert_equal(0, gitgutter#diff_highlight#common_prefix('-abcde', '+pqrst')) + call assert_equal(-1, gitgutter#diff_highlight#common_prefix('-abcde', '+pqrst')) + call assert_equal(-1, gitgutter#diff_highlight#common_prefix('abcde', 'pqrst')) " something in common - call assert_equal(3, gitgutter#diff_highlight#common_prefix('-abcde', '+abcpq')) + call assert_equal(-1, gitgutter#diff_highlight#common_prefix('-abcde', '+abcpq')) + call assert_equal(2, gitgutter#diff_highlight#common_prefix('abcde', 'abcpq')) + call assert_equal(0, gitgutter#diff_highlight#common_prefix('abc', 'apq')) " everything in common - call assert_equal(5, gitgutter#diff_highlight#common_prefix('-abcde', '+abcde')) + call assert_equal(-1, gitgutter#diff_highlight#common_prefix('-abcde', '+abcde')) + call assert_equal(4, gitgutter#diff_highlight#common_prefix('abcde', 'abcde')) " different lengths - call assert_equal(2, gitgutter#diff_highlight#common_prefix('-abcde', '+abx')) - call assert_equal(2, gitgutter#diff_highlight#common_prefix('-abx', '+abcde')) + call assert_equal(-1, gitgutter#diff_highlight#common_prefix('-abcde', '+abx')) + call assert_equal(1, gitgutter#diff_highlight#common_prefix('abcde', 'abx')) + call assert_equal(-1, gitgutter#diff_highlight#common_prefix('-abx', '+abcde')) + call assert_equal(1, gitgutter#diff_highlight#common_prefix('abx', 'abcde')) + call assert_equal(-1, gitgutter#diff_highlight#common_prefix('-abcde', '+abc')) + call assert_equal(2, gitgutter#diff_highlight#common_prefix('abcde', 'abc')) endfunction @@ -1010,5 +1018,12 @@ function Test_diff_highlight() " two edits let hunk = ['-The cat in the hat.', '+The ox in the box.'] - call assert_equal([[1, '-', 6, 8], [1, '-', 17, 19], [2, '+', 6, 7], [2, '+', 16, 18]], gitgutter#diff_highlight#process(hunk)) + call assert_equal([[1, '-', 6, 8], [2, '+', 6, 7], [1, '-', 17, 19], [2, '+', 16, 18]], gitgutter#diff_highlight#process(hunk)) +endfunction + + +function Test_lcs() + call assert_equal('', gitgutter#diff_highlight#lcs('', 'foo')) + call assert_equal('', gitgutter#diff_highlight#lcs('foo', '')) + call assert_equal('bar', gitgutter#diff_highlight#lcs('foobarbaz', 'bbart')) endfunction