diff_highlight.vim 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. " This is the minimum number of characters required between regions of change
  2. " in a line. It's somewhat arbitrary: higher values mean less visual busyness;
  3. " lower values mean more detail.
  4. let s:gap_between_regions = 5
  5. " Calculates the changed portions of lines.
  6. "
  7. " Based on:
  8. "
  9. " - diff-highlight (included with git)
  10. " https://github.com/git/git/blob/master/contrib/diff-highlight/DiffHighlight.pm
  11. "
  12. " - Diff Strategies, Neil Fraser
  13. " https://neil.fraser.name/writing/diff/
  14. " Returns a list of intra-line changed regions.
  15. " Each element is a list:
  16. "
  17. " [
  18. " line number (1-based),
  19. " type ('+' or '-'),
  20. " start column (1-based, inclusive),
  21. " stop column (1-based, inclusive),
  22. " ]
  23. "
  24. " Args:
  25. " hunk_body - list of lines
  26. function! gitgutter#diff_highlight#process(hunk_body)
  27. " Check whether we have the same number of lines added as removed.
  28. let [removed, added] = [0, 0]
  29. for line in a:hunk_body
  30. if line[0] == '-'
  31. let removed += 1
  32. elseif line[0] == '+'
  33. let added += 1
  34. endif
  35. endfor
  36. if removed != added
  37. return []
  38. endif
  39. let regions = []
  40. for i in range(removed)
  41. " pair lines by position
  42. let rline = a:hunk_body[i]
  43. let aline = a:hunk_body[i + removed]
  44. call s:diff(rline, aline, i, i+removed, 0, 0, regions, 1)
  45. endfor
  46. return regions
  47. endfunction
  48. function! s:diff(rline, aline, rlinenr, alinenr, rprefix, aprefix, regions, whole_line)
  49. " diff marker does not count as a difference in prefix
  50. let start = a:whole_line ? 1 : 0
  51. let prefix = s:common_prefix(a:rline[start:], a:aline[start:])
  52. if a:whole_line
  53. let prefix += 1
  54. endif
  55. let [rsuffix, asuffix] = s:common_suffix(a:rline, a:aline, prefix+1)
  56. " region of change (common prefix and suffix removed)
  57. let rtext = a:rline[prefix+1:rsuffix-1]
  58. let atext = a:aline[prefix+1:asuffix-1]
  59. " singular insertion
  60. if empty(rtext)
  61. if !a:whole_line || len(atext) != len(a:aline) " not whole line
  62. call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1, a:aprefix+asuffix+1-1])
  63. endif
  64. return
  65. endif
  66. " singular deletion
  67. if empty(atext)
  68. if !a:whole_line || len(rtext) != len(a:rline) " not whole line
  69. call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1, a:rprefix+rsuffix+1-1])
  70. endif
  71. return
  72. endif
  73. " two insertions
  74. let j = stridx(atext, rtext)
  75. if j != -1
  76. call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1, a:aprefix+prefix+j+1])
  77. call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1+j+len(rtext), a:aprefix+asuffix+1-1])
  78. return
  79. endif
  80. " two deletions
  81. let j = stridx(rtext, atext)
  82. if j != -1
  83. call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1, a:rprefix+prefix+j+1])
  84. call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1+j+len(atext), a:rprefix+rsuffix+1-1])
  85. return
  86. endif
  87. " two edits
  88. let lcs = s:lcs(rtext, atext)
  89. " TODO do we need to ensure we don't get more than 2 elements when splitting?
  90. if len(lcs) > s:gap_between_regions
  91. let redits = s:split(rtext, lcs)
  92. let aedits = s:split(atext, lcs)
  93. call s:diff(redits[0], aedits[0], a:rlinenr, a:alinenr, a:rprefix+prefix+1, a:aprefix+prefix+1, a:regions, 0)
  94. 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)
  95. return
  96. endif
  97. " fall back to highlighting entire changed area
  98. " if a change (but not the whole line)
  99. if !a:whole_line || ((prefix != 0 || rsuffix != len(a:rline)) && prefix+1 < rsuffix)
  100. call add(a:regions, [a:rlinenr+1, '-', a:rprefix+prefix+1+1, a:rprefix+rsuffix+1-1])
  101. endif
  102. " if a change (but not the whole line)
  103. if !a:whole_line || ((prefix != 0 || asuffix != len(a:aline)) && prefix+1 < asuffix)
  104. call add(a:regions, [a:alinenr+1, '+', a:aprefix+prefix+1+1, a:aprefix+asuffix+1-1])
  105. endif
  106. endfunction
  107. function! s:lcs(s1, s2)
  108. if empty(a:s1) || empty(a:s2)
  109. return ''
  110. endif
  111. let matrix = map(repeat([repeat([0], len(a:s2)+1)], len(a:s1)+1), 'copy(v:val)')
  112. let maxlength = 0
  113. let endindex = len(a:s1)
  114. for i in range(1, len(a:s1))
  115. for j in range(1, len(a:s2))
  116. if a:s1[i-1] ==# a:s2[j-1]
  117. let matrix[i][j] = 1 + matrix[i-1][j-1]
  118. if matrix[i][j] > maxlength
  119. let maxlength = matrix[i][j]
  120. let endindex = i - 1
  121. endif
  122. endif
  123. endfor
  124. endfor
  125. return a:s1[endindex - maxlength + 1 : endindex]
  126. endfunction
  127. " Returns 0-based index of last character of common prefix
  128. " If there is no common prefix, returns -1.
  129. "
  130. " a, b - strings
  131. "
  132. function! s:common_prefix(a, b)
  133. let len = min([len(a:a), len(a:b)])
  134. if len == 0
  135. return -1
  136. endif
  137. for i in range(len)
  138. if a:a[i:i] !=# a:b[i:i]
  139. return i - 1
  140. endif
  141. endfor
  142. return i
  143. endfunction
  144. " Returns 0-based indices of start of common suffix
  145. "
  146. " a, b - strings
  147. " start - 0-based index to start from
  148. function! s:common_suffix(a, b, start)
  149. let [sa, sb] = [len(a:a), len(a:b)]
  150. while sa >= a:start && sb >= a:start
  151. if a:a[sa] ==# a:b[sb]
  152. let sa -= 1
  153. let sb -= 1
  154. else
  155. break
  156. endif
  157. endwhile
  158. return [sa+1, sb+1]
  159. endfunction
  160. " Split a string on another string.
  161. " Assumes 1 occurrence of the delimiter.
  162. function! s:split(str, delimiter)
  163. let i = stridx(a:str, a:delimiter)
  164. if i == 0
  165. return ['', a:str[len(a:delimiter):]]
  166. endif
  167. return [a:str[:i-1], a:str[i+len(a:delimiter):]]
  168. endfunction