File: diff_highlight.vim

package info (click to toggle)
vim-gitgutter 0~20200414-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, forky, sid, trixie
  • size: 864 kB
  • sloc: sh: 14; makefile: 2
file content (225 lines) | stat: -rw-r--r-- 6,058 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
" 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