Files
vim-gitgutter/plugin/gitgutter.vim
Vaz Allen 7186f8bcad Highlighting customisation tweaks.
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" ;)
2013-03-08 11:14:56 +01:00

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: