diff.vim 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. let s:grep_available = executable('grep')
  2. if s:grep_available
  3. let s:grep_command = ' | '.(g:gitgutter_escape_grep ? '\grep' : 'grep')
  4. let s:grep_help = gitgutter#utility#system('grep --help')
  5. if s:grep_help =~# '--color'
  6. let s:grep_command .= ' --color=never'
  7. endif
  8. let s:grep_command .= ' -e '.gitgutter#utility#shellescape('^@@ ')
  9. endif
  10. let s:hunk_re = '^@@ -\(\d\+\),\?\(\d*\) +\(\d\+\),\?\(\d*\) @@'
  11. let s:fish = &shell =~# 'fish'
  12. function! gitgutter#diff#run_diff(realtime, use_external_grep)
  13. " Wrap compound commands in parentheses to make Windows happy.
  14. " bash doesn't mind the parentheses; fish doesn't want them.
  15. let cmd = s:fish ? '' : '('
  16. let bufnr = gitgutter#utility#bufnr()
  17. let tracked = getbufvar(bufnr, 'gitgutter_tracked') " i.e. tracked by git
  18. if !tracked
  19. let cmd .= 'git ls-files --error-unmatch '.gitgutter#utility#shellescape(gitgutter#utility#filename())
  20. let cmd .= s:fish ? '; and ' : ' && ('
  21. endif
  22. if a:realtime
  23. let blob_name = ':'.gitgutter#utility#shellescape(gitgutter#utility#file_relative_to_repo_root())
  24. let blob_file = tempname()
  25. let buff_file = tempname()
  26. let extension = gitgutter#utility#extension()
  27. if !empty(extension)
  28. let blob_file .= '.'.extension
  29. let buff_file .= '.'.extension
  30. endif
  31. let cmd .= 'git show '.blob_name.' > '.blob_file
  32. let cmd .= s:fish ? '; and ' : ' && '
  33. " Writing the whole buffer resets the '[ and '] marks and also the
  34. " 'modified' flag (if &cpoptions includes '+'). These are unwanted
  35. " side-effects so we save and restore the values ourselves.
  36. let modified = getbufvar(bufnr, "&mod")
  37. let op_mark_start = getpos("'[")
  38. let op_mark_end = getpos("']")
  39. execute 'keepalt silent write' buff_file
  40. call setbufvar(bufnr, "&mod", modified)
  41. call setpos("'[", op_mark_start)
  42. call setpos("']", op_mark_end)
  43. endif
  44. let cmd .= 'git diff --no-ext-diff --no-color -U0 '.g:gitgutter_diff_args.' -- '
  45. if a:realtime
  46. let cmd .= blob_file.' '.buff_file
  47. else
  48. let cmd .= gitgutter#utility#shellescape(gitgutter#utility#filename())
  49. endif
  50. if a:use_external_grep && s:grep_available
  51. let cmd .= s:grep_command
  52. endif
  53. if (a:use_external_grep && s:grep_available) || a:realtime
  54. " grep exits with 1 when no matches are found; diff exits with 1 when
  55. " differences are found. However we want to treat non-matches and
  56. " differences as non-erroneous behaviour; so we OR the command with one
  57. " which always exits with success (0).
  58. let cmd .= s:fish ? '; or ' : ' || '
  59. let cmd .= 'exit 0'
  60. endif
  61. if !s:fish
  62. let cmd .= ')'
  63. if !tracked
  64. let cmd .= ')'
  65. endif
  66. end
  67. let diff = gitgutter#utility#system(gitgutter#utility#command_in_directory_of_file(cmd))
  68. if a:realtime
  69. call delete(blob_file)
  70. call delete(buff_file)
  71. execute 'keepalt silent! bwipeout' buff_file
  72. endif
  73. if gitgutter#utility#shell_error()
  74. " A shell error indicates the file is not tracked by git (unless something bizarre is going on).
  75. throw 'diff failed'
  76. endif
  77. if !tracked
  78. call setbufvar(bufnr, 'gitgutter_tracked', 1)
  79. endif
  80. return diff
  81. endfunction
  82. function! gitgutter#diff#parse_diff(diff)
  83. let hunks = []
  84. for line in split(a:diff, '\n')
  85. let hunk_info = gitgutter#diff#parse_hunk(line)
  86. if len(hunk_info) == 4
  87. call add(hunks, hunk_info)
  88. endif
  89. endfor
  90. return hunks
  91. endfunction
  92. function! gitgutter#diff#parse_hunk(line)
  93. let matches = matchlist(a:line, s:hunk_re)
  94. if len(matches) > 0
  95. let from_line = str2nr(matches[1])
  96. let from_count = (matches[2] == '') ? 1 : str2nr(matches[2])
  97. let to_line = str2nr(matches[3])
  98. let to_count = (matches[4] == '') ? 1 : str2nr(matches[4])
  99. return [from_line, from_count, to_line, to_count]
  100. else
  101. return []
  102. end
  103. endfunction
  104. function! gitgutter#diff#process_hunks(hunks)
  105. call gitgutter#hunk#reset()
  106. let modified_lines = []
  107. for hunk in a:hunks
  108. call extend(modified_lines, gitgutter#diff#process_hunk(hunk))
  109. endfor
  110. return modified_lines
  111. endfunction
  112. " Returns [ [<line_number (number)>, <name (string)>], ...]
  113. function! gitgutter#diff#process_hunk(hunk)
  114. let modifications = []
  115. let from_line = a:hunk[0]
  116. let from_count = a:hunk[1]
  117. let to_line = a:hunk[2]
  118. let to_count = a:hunk[3]
  119. if gitgutter#diff#is_added(from_count, to_count)
  120. call gitgutter#diff#process_added(modifications, from_count, to_count, to_line)
  121. call gitgutter#hunk#increment_lines_added(to_count)
  122. elseif gitgutter#diff#is_removed(from_count, to_count)
  123. call gitgutter#diff#process_removed(modifications, from_count, to_count, to_line)
  124. call gitgutter#hunk#increment_lines_removed(from_count)
  125. elseif gitgutter#diff#is_modified(from_count, to_count)
  126. call gitgutter#diff#process_modified(modifications, from_count, to_count, to_line)
  127. call gitgutter#hunk#increment_lines_modified(to_count)
  128. elseif gitgutter#diff#is_modified_and_added(from_count, to_count)
  129. call gitgutter#diff#process_modified_and_added(modifications, from_count, to_count, to_line)
  130. call gitgutter#hunk#increment_lines_added(to_count - from_count)
  131. call gitgutter#hunk#increment_lines_modified(from_count)
  132. elseif gitgutter#diff#is_modified_and_removed(from_count, to_count)
  133. call gitgutter#diff#process_modified_and_removed(modifications, from_count, to_count, to_line)
  134. call gitgutter#hunk#increment_lines_modified(to_count)
  135. call gitgutter#hunk#increment_lines_removed(from_count - to_count)
  136. endif
  137. return modifications
  138. endfunction
  139. function! gitgutter#diff#is_added(from_count, to_count)
  140. return a:from_count == 0 && a:to_count > 0
  141. endfunction
  142. function! gitgutter#diff#is_removed(from_count, to_count)
  143. return a:from_count > 0 && a:to_count == 0
  144. endfunction
  145. function! gitgutter#diff#is_modified(from_count, to_count)
  146. return a:from_count > 0 && a:to_count > 0 && a:from_count == a:to_count
  147. endfunction
  148. function! gitgutter#diff#is_modified_and_added(from_count, to_count)
  149. return a:from_count > 0 && a:to_count > 0 && a:from_count < a:to_count
  150. endfunction
  151. function! gitgutter#diff#is_modified_and_removed(from_count, to_count)
  152. return a:from_count > 0 && a:to_count > 0 && a:from_count > a:to_count
  153. endfunction
  154. function! gitgutter#diff#process_added(modifications, from_count, to_count, to_line)
  155. let offset = 0
  156. while offset < a:to_count
  157. let line_number = a:to_line + offset
  158. call add(a:modifications, [line_number, 'added'])
  159. let offset += 1
  160. endwhile
  161. endfunction
  162. function! gitgutter#diff#process_removed(modifications, from_count, to_count, to_line)
  163. if a:to_line == 0
  164. call add(a:modifications, [1, 'removed_first_line'])
  165. else
  166. call add(a:modifications, [a:to_line, 'removed'])
  167. endif
  168. endfunction
  169. function! gitgutter#diff#process_modified(modifications, from_count, to_count, to_line)
  170. let offset = 0
  171. while offset < a:to_count
  172. let line_number = a:to_line + offset
  173. call add(a:modifications, [line_number, 'modified'])
  174. let offset += 1
  175. endwhile
  176. endfunction
  177. function! gitgutter#diff#process_modified_and_added(modifications, from_count, to_count, to_line)
  178. let offset = 0
  179. while offset < a:from_count
  180. let line_number = a:to_line + offset
  181. call add(a:modifications, [line_number, 'modified'])
  182. let offset += 1
  183. endwhile
  184. while offset < a:to_count
  185. let line_number = a:to_line + offset
  186. call add(a:modifications, [line_number, 'added'])
  187. let offset += 1
  188. endwhile
  189. endfunction
  190. function! gitgutter#diff#process_modified_and_removed(modifications, from_count, to_count, to_line)
  191. let offset = 0
  192. while offset < a:to_count
  193. let line_number = a:to_line + offset
  194. call add(a:modifications, [line_number, 'modified'])
  195. let offset += 1
  196. endwhile
  197. let a:modifications[-1] = [a:to_line + offset - 1, 'modified_removed']
  198. endfunction
  199. " Generates a zero-context diff for the current hunk.
  200. "
  201. " type - stage | revert | preview
  202. function! gitgutter#diff#generate_diff_for_hunk(type)
  203. " Although (we assume) diff is up to date, we don't store it anywhere so we
  204. " have to regenerate it now...
  205. let diff = gitgutter#diff#run_diff(0, 0)
  206. let diff_for_hunk = gitgutter#diff#discard_hunks(diff, a:type == 'stage' || a:type == 'revert')
  207. if a:type == 'stage' || a:type == 'revert'
  208. let diff_for_hunk = gitgutter#diff#adjust_hunk_summary(diff_for_hunk, a:type == 'stage')
  209. endif
  210. return diff_for_hunk
  211. endfunction
  212. " Returns the diff with all hunks discarded except the current.
  213. "
  214. " diff - the diff to process
  215. " keep_header - truthy to keep the diff header and hunk summary, falsy to discard it
  216. function! gitgutter#diff#discard_hunks(diff, keep_header)
  217. let modified_diff = []
  218. let keep_line = a:keep_header
  219. for line in split(a:diff, '\n')
  220. let hunk_info = gitgutter#diff#parse_hunk(line)
  221. if len(hunk_info) == 4 " start of new hunk
  222. let keep_line = gitgutter#hunk#cursor_in_hunk(hunk_info)
  223. endif
  224. if keep_line
  225. call add(modified_diff, line)
  226. endif
  227. endfor
  228. if a:keep_header
  229. return join(modified_diff, "\n") . "\n"
  230. else
  231. " Discard hunk summary too.
  232. return join(modified_diff[1:], "\n") . "\n"
  233. endif
  234. endfunction
  235. " Adjust hunk summary (from's / to's line number) to ignore changes above/before this one.
  236. "
  237. " diff_for_hunk - a diff containing only the hunk of interest
  238. " staging - truthy if the hunk is to be staged, falsy if it is to be reverted
  239. "
  240. " TODO: push this down to #discard_hunks?
  241. function! gitgutter#diff#adjust_hunk_summary(diff_for_hunk, staging)
  242. let line_adjustment = gitgutter#hunk#line_adjustment_for_current_hunk()
  243. let adj_diff = []
  244. for line in split(a:diff_for_hunk, '\n')
  245. if match(line, s:hunk_re) != -1
  246. if a:staging
  247. " increment 'to' line number
  248. let line = substitute(line, '+\@<=\(\d\+\)', '\=submatch(1)+line_adjustment', '')
  249. else
  250. " decrement 'from' line number
  251. let line = substitute(line, '-\@<=\(\d\+\)', '\=submatch(1)-line_adjustment', '')
  252. endif
  253. endif
  254. call add(adj_diff, line)
  255. endfor
  256. return join(adj_diff, "\n") . "\n"
  257. endfunction