mirror of
https://github.com/airblade/vim-gitgutter.git
synced 2025-11-08 11:33:48 -05:00
Highlight groups for signs: - renamed highlight groups with `GitGutter` prefix, and following the naming style of the builtin groups `DiffAdd`, `DiffChange`, etc. - using `highlight default link` to set default colours for signs. e.g. the plugin defines `GitGutterAddDefault` as green, and default (if `g:gitgutter_highlights`) links `GitGutterAdd` to it (which the user can customise). Highlight groups for lines: - added gitgutter-specific highlight groups for line highlighting (`GitGutterAddLine`, etc) so that this highlighting can be customised independent of `DiffAdd` et al (which these default link to). - line highlighting toggling now only changes the linehl attribute. The `linehl` attribute is toggled in `s:update_line_highlights`, which separates concerns more clearly. I'm thinking ahead to the possibility of the sign text being configurable, and thinking it will be DRYer to keep these things separate. Maybe it's just aesthetic though. Bonus: resolved "eugh" ;)
441 lines
13 KiB
VimL
441 lines
13 KiB
VimL
if exists('g:loaded_gitgutter') || !executable('git') || !has('signs') || &cp
|
|
finish
|
|
endif
|
|
let g:loaded_gitgutter = 1
|
|
|
|
" Initialisation {{{
|
|
|
|
if !exists('g:gitgutter_enabled')
|
|
let g:gitgutter_enabled = 1
|
|
endif
|
|
|
|
if !exists('g:gitgutter_highlights')
|
|
let g:gitgutter_highlights = 1
|
|
endif
|
|
|
|
if !exists('g:gitgutter_highlight_lines')
|
|
let g:gitgutter_highlight_lines = 0
|
|
endif
|
|
|
|
function! s:init()
|
|
if !exists('g:gitgutter_initialised')
|
|
let s:highlight_lines = g:gitgutter_highlight_lines
|
|
call s:define_signs()
|
|
|
|
call s:define_highlights()
|
|
|
|
" Vim doesn't namespace sign ids so every plugin shares the same
|
|
" namespace. Sign ids are simply integers so to avoid clashes with other
|
|
" signs we guess at a clear run.
|
|
"
|
|
" Note also we currently never reset s:next_sign_id.
|
|
let s:first_sign_id = 3000
|
|
let s:next_sign_id = s:first_sign_id
|
|
let s:sign_ids = {} " key: filename, value: list of sign ids
|
|
let s:other_signs = []
|
|
|
|
let g:gitgutter_initialised = 1
|
|
endif
|
|
endfunction
|
|
|
|
function! s:define_highlights()
|
|
" sign highlights
|
|
hi GitGutterAddDefault guifg=#009900 guibg=NONE ctermfg=2 ctermbg=NONE
|
|
hi GitGutterChangeDefault guifg=#bbbb00 guibg=NONE ctermfg=3 ctermbg=NONE
|
|
hi GitGutterDeleteDefault guifg=#ff2222 guibg=NONE ctermfg=1 ctermbg=NONE
|
|
hi default link GitGutterChangeDeleteDefault GitGutterChangeDefault
|
|
|
|
if g:gitgutter_highlights
|
|
hi default link GitGutterAdd GitGutterAddDefault
|
|
hi default link GitGutterChange GitGutterChangeDefault
|
|
hi default link GitGutterDelete GitGutterDeleteDefault
|
|
hi default link GitGutterChangeDelete GitGutterChangeDeleteDefault
|
|
endif
|
|
|
|
" line highlight defaults, meant to be user-edited
|
|
hi default link GitGutterAddLine DiffAdd
|
|
hi default link GitGutterChangeLine DiffChange
|
|
hi default link GitGutterDeleteLine DiffDelete
|
|
hi default link GitGutterChangeDeleteLine GitGutterChangeLineDefault
|
|
|
|
call s:update_line_highlights(s:highlight_lines)
|
|
endfunction
|
|
|
|
function! s:define_signs()
|
|
sign define GitGutterLineAdded text=+ texthl=GitGutterAdd linehl=
|
|
sign define GitGutterLineModified text=~ texthl=GitGutterChange linehl=
|
|
sign define GitGutterLineRemoved text=_ texthl=GitGutterDelete linehl=
|
|
sign define GitGutterLineModifiedRemoved text=~_ texthl=GitGutterChangeDelete linehl=
|
|
endfunction
|
|
|
|
" }}}
|
|
|
|
" Utility {{{
|
|
|
|
function! s:is_active()
|
|
return g:gitgutter_enabled && s:exists_current_file() && s:is_in_a_git_repo() && s:is_tracked_by_git()
|
|
endfunction
|
|
|
|
function! s:update_line_highlights(highlight_lines)
|
|
let s:highlight_lines = a:highlight_lines
|
|
if s:highlight_lines
|
|
sign define GitGutterLineAdded linehl=GitGutterAddLine
|
|
sign define GitGutterLineModified linehl=GitGutterChangeLine
|
|
sign define GitGutterLineRemoved linehl=GitGutterDeleteLine
|
|
sign define GitGutterLineModifiedRemoved linehl=GitGutterChangeDeleteLine
|
|
else
|
|
sign define GitGutterLineAdded linehl=
|
|
sign define GitGutterLineModified linehl=
|
|
sign define GitGutterLineRemoved linehl=
|
|
sign define GitGutterLineModifiedRemoved linehl=
|
|
endif
|
|
redraw!
|
|
endfunction
|
|
|
|
function! s:current_file()
|
|
return expand("%:p")
|
|
endfunction
|
|
|
|
function! s:exists_current_file()
|
|
return strlen(s:current_file()) > 0
|
|
endfunction
|
|
|
|
function! s:directory_of_current_file()
|
|
return shellescape(expand("%:p:h"))
|
|
endfunction
|
|
|
|
function! s:discard_stdout_and_stderr()
|
|
if !exists('s:discard')
|
|
if &shellredir ==? '>%s 2>&1'
|
|
let s:discard = ' > /dev/null 2>&1'
|
|
else
|
|
let s:discard = ' >& /dev/null'
|
|
endif
|
|
endif
|
|
return s:discard
|
|
endfunction
|
|
|
|
function! s:command_in_directory_of_current_file(cmd)
|
|
return 'cd ' . s:directory_of_current_file() . ' && ' . a:cmd
|
|
endfunction
|
|
|
|
function! s:is_in_a_git_repo()
|
|
let cmd = 'git rev-parse' . s:discard_stdout_and_stderr()
|
|
call system(s:command_in_directory_of_current_file(cmd))
|
|
return !v:shell_error
|
|
endfunction
|
|
|
|
function! s:is_tracked_by_git()
|
|
let cmd = 'git ls-files --error-unmatch' . s:discard_stdout_and_stderr() . ' ' . shellescape(s:current_file())
|
|
call system(s:command_in_directory_of_current_file(cmd))
|
|
return !v:shell_error
|
|
endfunction
|
|
|
|
" }}}
|
|
|
|
" Diff processing {{{
|
|
|
|
function! s:run_diff()
|
|
let cmd = 'git diff --no-ext-diff --no-color -U0 ' . shellescape(s:current_file()) .
|
|
\ ' | grep -e "^@@ "'
|
|
let diff = system(s:command_in_directory_of_current_file(cmd))
|
|
return diff
|
|
endfunction
|
|
|
|
function! s:parse_diff(diff)
|
|
let hunk_re = '^@@ -\(\d\+\),\?\(\d*\) +\(\d\+\),\?\(\d*\) @@'
|
|
let hunks = []
|
|
for line in split(a:diff, '\n')
|
|
let matches = matchlist(line, hunk_re)
|
|
if len(matches) > 0
|
|
let from_line = str2nr(matches[1])
|
|
let from_count = (matches[2] == '') ? 1 : str2nr(matches[2])
|
|
let to_line = str2nr(matches[3])
|
|
let to_count = (matches[4] == '') ? 1 : str2nr(matches[4])
|
|
call add(hunks, [from_line, from_count, to_line, to_count])
|
|
endif
|
|
endfor
|
|
return hunks
|
|
endfunction
|
|
|
|
function! s:process_hunks(hunks)
|
|
let modified_lines = []
|
|
for hunk in a:hunks
|
|
call extend(modified_lines, s:process_hunk(hunk))
|
|
endfor
|
|
return modified_lines
|
|
endfunction
|
|
|
|
function! s:process_hunk(hunk)
|
|
let modifications = []
|
|
let from_line = a:hunk[0]
|
|
let from_count = a:hunk[1]
|
|
let to_line = a:hunk[2]
|
|
let to_count = a:hunk[3]
|
|
|
|
if s:is_added(from_count, to_count)
|
|
call s:process_added(modifications, from_count, to_count, to_line)
|
|
|
|
elseif s:is_removed(from_count, to_count)
|
|
call s:process_removed(modifications, from_count, to_count, to_line)
|
|
|
|
elseif s:is_modified(from_count, to_count)
|
|
call s:process_modified(modifications, from_count, to_count, to_line)
|
|
|
|
elseif s:is_modified_and_added(from_count, to_count)
|
|
call s:process_modified_and_added(modifications, from_count, to_count, to_line)
|
|
|
|
elseif s:is_modified_and_removed(from_count, to_count)
|
|
call s:process_modified_and_removed(modifications, from_count, to_count, to_line)
|
|
|
|
endif
|
|
return modifications
|
|
endfunction
|
|
|
|
" }}}
|
|
|
|
" Diff utility {{{
|
|
|
|
function! s:is_added(from_count, to_count)
|
|
return a:from_count == 0 && a:to_count > 0
|
|
endfunction
|
|
|
|
function! s:is_removed(from_count, to_count)
|
|
return a:from_count > 0 && a:to_count == 0
|
|
endfunction
|
|
|
|
function! s:is_modified(from_count, to_count)
|
|
return a:from_count > 0 && a:to_count > 0 && a:from_count == a:to_count
|
|
endfunction
|
|
|
|
function! s:is_modified_and_added(from_count, to_count)
|
|
return a:from_count > 0 && a:to_count > 0 && a:from_count < a:to_count
|
|
endfunction
|
|
|
|
function! s:is_modified_and_removed(from_count, to_count)
|
|
return a:from_count > 0 && a:to_count > 0 && a:from_count > a:to_count
|
|
endfunction
|
|
|
|
function! s:process_added(modifications, from_count, to_count, to_line)
|
|
let offset = 0
|
|
while offset < a:to_count
|
|
let line_number = a:to_line + offset
|
|
call add(a:modifications, [line_number, 'added'])
|
|
let offset += 1
|
|
endwhile
|
|
endfunction
|
|
|
|
function! s:process_removed(modifications, from_count, to_count, to_line)
|
|
call add(a:modifications, [a:to_line, 'removed'])
|
|
endfunction
|
|
|
|
function! s:process_modified(modifications, from_count, to_count, to_line)
|
|
let offset = 0
|
|
while offset < a:to_count
|
|
let line_number = a:to_line + offset
|
|
call add(a:modifications, [line_number, 'modified'])
|
|
let offset += 1
|
|
endwhile
|
|
endfunction
|
|
|
|
function! s:process_modified_and_added(modifications, from_count, to_count, to_line)
|
|
let offset = 0
|
|
while offset < a:from_count
|
|
let line_number = a:to_line + offset
|
|
call add(a:modifications, [line_number, 'modified'])
|
|
let offset += 1
|
|
endwhile
|
|
while offset < a:to_count
|
|
let line_number = a:to_line + offset
|
|
call add(a:modifications, [line_number, 'added'])
|
|
let offset += 1
|
|
endwhile
|
|
endfunction
|
|
|
|
function! s:process_modified_and_removed(modifications, from_count, to_count, to_line)
|
|
let offset = 0
|
|
while offset < a:to_count
|
|
let line_number = a:to_line + offset
|
|
call add(a:modifications, [line_number, 'modified'])
|
|
let offset += 1
|
|
endwhile
|
|
call add(a:modifications, [a:to_line + offset - 1, 'modified_removed'])
|
|
endfunction
|
|
|
|
" }}}
|
|
|
|
" Sign processing {{{
|
|
|
|
function! s:clear_signs(file_name)
|
|
if exists('s:sign_ids') && has_key(s:sign_ids, a:file_name)
|
|
for id in s:sign_ids[a:file_name]
|
|
exe ":sign unplace " . id . " file=" . a:file_name
|
|
endfor
|
|
let s:sign_ids[a:file_name] = []
|
|
endif
|
|
endfunction
|
|
|
|
" This assumes there are no GitGutter signs in the current file.
|
|
" If this is untenable we could change the regexp to exclude GitGutter's
|
|
" signs.
|
|
function! s:find_other_signs(file_name)
|
|
redir => signs
|
|
silent exe ":sign place file=" . a:file_name
|
|
redir END
|
|
let s:other_signs = []
|
|
for sign_line in split(signs, '\n')
|
|
if sign_line =~ '^\s\+line'
|
|
let matches = matchlist(sign_line, '^\s\+line=\(\d\+\)')
|
|
let line_number = str2nr(matches[1])
|
|
call add(s:other_signs, line_number)
|
|
endif
|
|
endfor
|
|
endfunction
|
|
|
|
function! s:show_signs(file_name, modified_lines)
|
|
for line in a:modified_lines
|
|
let line_number = line[0]
|
|
" snake case to camel case
|
|
let type = substitute(line[1], '\v(.)(\a+)(_(.)(.+))?', '\u\1\l\2\u\4\l\5', '')
|
|
call s:add_sign(line_number, 'GitGutterLine' . type, a:file_name)
|
|
endfor
|
|
endfunction
|
|
|
|
function! s:add_sign(line_number, name, file_name)
|
|
let id = s:next_sign_id()
|
|
if !s:is_other_sign(a:line_number) " Don't clobber other people's signs.
|
|
exe ":sign place " . id . " line=" . a:line_number . " name=" . a:name . " file=" . a:file_name
|
|
call s:remember_sign(id, a:file_name)
|
|
endif
|
|
endfunction
|
|
|
|
function! s:next_sign_id()
|
|
let next_id = s:next_sign_id
|
|
let s:next_sign_id += 1
|
|
return next_id
|
|
endfunction
|
|
|
|
function! s:remember_sign(id, file_name)
|
|
if has_key(s:sign_ids, a:file_name)
|
|
let sign_ids_for_current_file = s:sign_ids[a:file_name]
|
|
call add(sign_ids_for_current_file, a:id)
|
|
else
|
|
let sign_ids_for_current_file = [a:id]
|
|
endif
|
|
let s:sign_ids[a:file_name] = sign_ids_for_current_file
|
|
endfunction
|
|
|
|
function! s:is_other_sign(line_number)
|
|
return index(s:other_signs, a:line_number) == -1 ? 0 : 1
|
|
endfunction
|
|
|
|
" }}}
|
|
|
|
" Public interface {{{
|
|
|
|
function! GitGutter()
|
|
if s:is_active()
|
|
call s:init()
|
|
let diff = s:run_diff()
|
|
let s:hunks = s:parse_diff(diff)
|
|
let modified_lines = s:process_hunks(s:hunks)
|
|
let file_name = s:current_file()
|
|
call s:clear_signs(file_name)
|
|
call s:find_other_signs(file_name)
|
|
call s:show_signs(file_name, modified_lines)
|
|
endif
|
|
endfunction
|
|
command GitGutter call GitGutter()
|
|
|
|
function! GitGutterDisable()
|
|
let g:gitgutter_enabled = 0
|
|
call s:clear_signs(s:current_file())
|
|
endfunction
|
|
command GitGutterDisable call GitGutterDisable()
|
|
|
|
function! GitGutterEnable()
|
|
let g:gitgutter_enabled = 1
|
|
call GitGutter()
|
|
endfunction
|
|
command GitGutterEnable call GitGutterEnable()
|
|
|
|
function! GitGutterToggle()
|
|
if g:gitgutter_enabled
|
|
call GitGutterDisable()
|
|
else
|
|
call GitGutterEnable()
|
|
endif
|
|
endfunction
|
|
command GitGutterToggle call GitGutterToggle()
|
|
|
|
function! GitGutterLineHighlightsDisable()
|
|
call s:update_line_highlights(0)
|
|
endfunction
|
|
command GitGutterLineHighlightsDisable call GitGutterLineHighlightsDisable()
|
|
|
|
function! GitGutterLineHighlightsEnable()
|
|
call s:update_line_highlights(1)
|
|
endfunction
|
|
command GitGutterLineHighlightsEnable call GitGutterLineHighlightsEnable()
|
|
|
|
function! GitGutterLineHighlightsToggle()
|
|
call s:update_line_highlights(s:highlight_lines ? 0 : 1)
|
|
endfunction
|
|
command GitGutterLineHighlightsToggle call GitGutterLineHighlightsToggle()
|
|
|
|
function! GitGutterNextHunk()
|
|
if s:is_active()
|
|
let current_line = line('.')
|
|
for hunk in s:hunks
|
|
if hunk[2] > current_line
|
|
execute 'normal! ' . hunk[2] . 'G'
|
|
break
|
|
endif
|
|
endfor
|
|
endif
|
|
endfunction
|
|
command GitGutterNextHunk call GitGutterNextHunk()
|
|
|
|
function! GitGutterPrevHunk()
|
|
if s:is_active()
|
|
let current_line = line('.')
|
|
for hunk in reverse(copy(s:hunks))
|
|
if hunk[2] < current_line
|
|
execute 'normal! ' . hunk[2] . 'G'
|
|
break
|
|
endif
|
|
endfor
|
|
endif
|
|
endfunction
|
|
command GitGutterPrevHunk call GitGutterPrevHunk()
|
|
|
|
" Returns the git-diff hunks for the current file or an empty list if there
|
|
" aren't any hunks.
|
|
"
|
|
" The return value is a list of lists. There is one inner list per hunk.
|
|
"
|
|
" [
|
|
" [from_line, from_count, to_line, to_count],
|
|
" [from_line, from_count, to_line, to_count],
|
|
" ...
|
|
" ]
|
|
"
|
|
" where:
|
|
"
|
|
" `from` - refers to the staged file
|
|
" `to` - refers to the working tree's file
|
|
" `line` - refers to the line number where the change starts
|
|
" `count` - refers to the number of lines the change covers
|
|
function! GitGutterGetHunks()
|
|
return s:is_active() ? s:hunks : []
|
|
endfunction
|
|
|
|
augroup gitgutter
|
|
autocmd!
|
|
autocmd BufReadPost,BufWritePost,FileReadPost,FileWritePost,FocusGained * call GitGutter()
|
|
augroup END
|
|
|
|
" }}}
|
|
|
|
" vim:set et sw=2 fdm=marker:
|