Enable intra-line highlights to handle multiple regions

For example consider:

    -The cat in the hat.
    +The ox in the box.

Before this change the highlights would be:

    -The cat in the hat.
         ^^^^^^^^^^^^^^
    +The ox in the box.
         ^^^^^^^^^^^^^

After this change the highlights are:

    -The cat in the hat.
         ^^^        ^^^
    +The ox in the box.
         ^^        ^^^

Another example; before:

    -The quick brown fox jumped
    +The (quick) brown (fox) jumped
         ^^^^^^^^^^^^^^^^^^^

And after:

    -The quick brown fox jumped
    +The (quick) brown (fox) jumped
         ^     ^       ^   ^
This commit is contained in:
Andy Stewart
2019-09-24 10:26:31 +01:00
parent 68e735b92c
commit 9231bda97f
2 changed files with 130 additions and 58 deletions

View File

@@ -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

View File

@@ -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