diff --git a/README.mkd b/README.mkd index ee2d3f1..66539df 100644 --- a/README.mkd +++ b/README.mkd @@ -12,6 +12,7 @@ Features: * Never saves the buffer. * Quick jumping between blocks of changed lines ("hunks"). * Stage/undo/preview individual hunks. +* Previews highlight intra-line changes. * Stage partial hunks. * Provides a hunk text object. * Diffs against index (default) or any commit. diff --git a/autoload/gitgutter/diff_highlight.vim b/autoload/gitgutter/diff_highlight.vim new file mode 100644 index 0000000..9004a78 --- /dev/null +++ b/autoload/gitgutter/diff_highlight.vim @@ -0,0 +1,100 @@ +" Calculates the changed portions of lines. Based closely on diff-highlight +" (included with git) - please note its caveats. +" +" https://github.com/git/git/blob/master/contrib/diff-highlight/DiffHighlight.pm + + +" 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] + + let prefix = s:common_prefix(rline, aline) + let [rsuffix, asuffix] = s:common_suffix(rline, aline, prefix+1) + + if (prefix != 0 || rsuffix != 0) && prefix+1 < rsuffix + call add(regions, [i+1, '-', prefix+1+1, rsuffix+1-1]) + endif + + if (prefix != 0 || asuffix != 0) && prefix+1 < asuffix + call add(regions, [i+1+removed, '+', prefix+1+1, asuffix+1-1]) + endif + endfor + + return regions +endfunction + + +" Returns 0-based index of last character of common prefix +" Does not treat leading +/- as different. +" +" 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) + 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 diff --git a/autoload/gitgutter/highlight.vim b/autoload/gitgutter/highlight.vim index 5b14fc3..b7ee64b 100644 --- a/autoload/gitgutter/highlight.vim +++ b/autoload/gitgutter/highlight.vim @@ -102,6 +102,10 @@ function! gitgutter#highlight#define_highlights() abort highlight default link GitGutterChangeLineNr CursorLineNr highlight default link GitGutterDeleteLineNr CursorLineNr highlight default link GitGutterChangeDeleteLineNr CursorLineNr + + " Highlights used intra line. + highlight GitGutterAddIntraLine gui=reverse + highlight GitGutterDeleteIntraLine gui=reverse endfunction function! gitgutter#highlight#define_signs() abort diff --git a/autoload/gitgutter/hunk.vim b/autoload/gitgutter/hunk.vim index d34546f..d196d68 100644 --- a/autoload/gitgutter/hunk.vim +++ b/autoload/gitgutter/hunk.vim @@ -458,11 +458,20 @@ function! s:populate_hunk_preview_window(header, body) call nvim_buf_set_lines(winbufnr(s:winid), 0, -1, v:false, []) call nvim_buf_set_lines(winbufnr(s:winid), 0, -1, v:false, a:body) call nvim_buf_set_option(winbufnr(s:winid), 'modified', v:false) + + let ns_id = nvim_create_namespace('GitGutter') + call nvim_buf_clear_namespace(winbufnr(s:winid), ns_id, 0, -1) + for region in gitgutter#diff_highlight#process(a:body) + let group = region[1] == '+' ? 'GitGutterAddIntraLine' : 'GitGutterDeleteIntraLine' + call nvim_buf_add_highlight(winbufnr(s:winid), ns_id, group, region[0]-1, region[2]-1, region[3]) + endfor + call nvim_win_set_cursor(s:winid, [1,0]) endif if exists('*popup_create') call popup_settext(s:winid, a:body) + " TODO add intra line highlights endif else @@ -472,6 +481,13 @@ function! s:populate_hunk_preview_window(header, body) %delete _ call setline(1, a:body) setlocal nomodified + + call clearmatches() + for region in gitgutter#diff_highlight#process(a:body) + let group = region[1] == '+' ? 'GitGutterAddIntraLine' : 'GitGutterDeleteIntraLine' + call matchaddpos(group, [[region[0], region[2], region[3]-region[2]+1]]) + endfor + 1 endif endfunction diff --git a/test/test_gitgutter.vim b/test/test_gitgutter.vim index f35be58..f978f2d 100644 --- a/test/test_gitgutter.vim +++ b/test/test_gitgutter.vim @@ -60,6 +60,9 @@ function SetUp() execute ':cd' s:test_repo edit! fixture.txt call gitgutter#sign#reset() + + " FIXME why won't vim autoload the file? + execute 'source' '../../autoload/gitgutter/diff_highlight.vim' endfunction function TearDown() @@ -910,3 +913,62 @@ function Test_quickfix() call s:assert_list_of_dicts(expected, getqflist()) endfunction + + +function Test_common_prefix() + " nothing in common + call assert_equal(0, gitgutter#diff_highlight#common_prefix('-abcde', '+pqrst')) + " something in common + call assert_equal(3, gitgutter#diff_highlight#common_prefix('-abcde', '+abcpq')) + " everything in common + call assert_equal(5, 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')) +endfunction + + +function Test_common_suffix() + " nothing in common + call assert_equal([6,6], gitgutter#diff_highlight#common_suffix('-abcde', '+pqrst', 0)) + " something in common + call assert_equal([3,3], gitgutter#diff_highlight#common_suffix('-abcde', '+pqcde', 0)) + " everything in common + call assert_equal([5,5], gitgutter#diff_highlight#common_suffix('-abcde', '+abcde', 5)) + " different lengths + call assert_equal([4,2], gitgutter#diff_highlight#common_suffix('-abcde', '+xde', 0)) + call assert_equal([2,4], gitgutter#diff_highlight#common_suffix('-xde', '+abcde', 0)) +endfunction + + +function Test_diff_highlight() + " Ignores mismatched number of added and removed lines. + call assert_equal([], gitgutter#diff_highlight#process(['-foo'])) + call assert_equal([], gitgutter#diff_highlight#process(['+foo'])) + call assert_equal([], gitgutter#diff_highlight#process(['-foo','-bar','+baz'])) + + " change in middle + let hunk = ['-foo bar baz', '+foo (bar) baz'] + let expected = [[1, '-', 6, 8], [2, '+', 6, 10]] + call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) + + " change at start + let hunk = ['-foo bar baz', '+(foo) bar baz'] + let expected = [[1, '-', 2, 4], [2, '+', 2, 6]] + call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) + + " change at end + let hunk = ['-foo bar baz', '+foo bar (baz)'] + let expected = [[1, '-', 10, 12], [2, '+', 10, 14]] + call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) + + " removed in middle + let hunk = ['-foo bar baz', '+foo baz'] + let expected = [[1, '-', 8, 11]] + call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) + + " added in middle + let hunk = ['-foo baz', '+foo bar baz'] + let expected = [[2, '+', 8, 11]] + call assert_equal(expected, gitgutter#diff_highlight#process(hunk)) +endfunction