Files
vim-gitgutter/plugin/gitgutter.vim
2013-03-20 09:29:00 +01:00

548 lines
15 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_highlight_lines')
let g:gitgutter_highlight_lines = 0
endif
let s:highlight_lines = g:gitgutter_highlight_lines
if !exists('g:gitgutter_sign_column_always')
let g:gitgutter_sign_column_always = 0
endif
if !exists('g:gitgutter_on_bufenter')
let g:gitgutter_on_bufenter = 1
endif
if !exists('g:gitgutter_all_on_focusgained')
let g:gitgutter_all_on_focusgained = 1
endif
if !exists('g:gitgutter_sign_added')
let g:gitgutter_sign_added = '+'
endif
if !exists('g:gitgutter_sign_modified')
let g:gitgutter_sign_modified = '~'
endif
if !exists('g:gitgutter_sign_removed')
let g:gitgutter_sign_removed = '_'
endif
if !exists('g:gitgutter_sign_modified_removed')
let g:gitgutter_sign_modified_removed = '~_'
endif
let s:file = ''
function! s:init()
if !exists('g:gitgutter_initialised')
call s:define_sign_column_highlight()
call s:define_highlights()
call s:define_signs()
" 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 s:dummy_sign_id = 153
let g:gitgutter_initialised = 1
endif
endfunction
" }}}
" Utility {{{
function! s:is_active()
return g:gitgutter_enabled && s:exists_file() && s:is_in_a_git_repo() && s:is_tracked_by_git()
endfunction
function! s:current_file()
return expand('%:p')
endfunction
function! s:set_file(file)
let s:file = a:file
endfunction
function! s:file()
return s:file
endfunction
function! s:exists_file()
return filereadable(s:file())
endfunction
function! s:directory_of_file()
return shellescape(fnamemodify(s:file(), ':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_file(cmd)
return 'cd ' . s:directory_of_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_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:file())
call system(s:command_in_directory_of_file(cmd))
return !v:shell_error
endfunction
function! s:snake_case_to_camel_case(text)
return substitute(a:text, '\v(.)(\a+)(_(.)(.+))?', '\u\1\l\2\u\4\l\5', '')
endfunction
function! s:buffers()
return filter(range(1, bufnr('$')), 'buflisted(v:val)')
endfunction
function! s:visible_buffers()
let visible = []
for i in range(tabpagenr('$'))
call extend(visible, tabpagebuflist(i + 1))
endfor
return visible
endfunction
" }}}
" Highlights and signs {{{
function! s:define_sign_column_highlight()
highlight default link SignColumn LineNr
endfunction
function! s:define_highlights()
" Highlights used by the signs.
highlight GitGutterAddDefault guifg=#009900 guibg=NONE ctermfg=2 ctermbg=NONE
highlight GitGutterChangeDefault guifg=#bbbb00 guibg=NONE ctermfg=3 ctermbg=NONE
highlight GitGutterDeleteDefault guifg=#ff2222 guibg=NONE ctermfg=1 ctermbg=NONE
highlight default link GitGutterChangeDeleteDefault GitGutterChangeDefault
highlight default link GitGutterAdd GitGutterAddDefault
highlight default link GitGutterChange GitGutterChangeDefault
highlight default link GitGutterDelete GitGutterDeleteDefault
highlight default link GitGutterChangeDelete GitGutterChangeDeleteDefault
" Highlights used for the whole line.
highlight default link GitGutterAddLine DiffAdd
highlight default link GitGutterChangeLine DiffChange
highlight default link GitGutterDeleteLine DiffDelete
highlight default link GitGutterChangeDeleteLine GitGutterChangeLineDefault
endfunction
function! s:define_signs()
sign define GitGutterLineAdded
sign define GitGutterLineModified
sign define GitGutterLineRemoved
sign define GitGutterLineModifiedRemoved
sign define GitGutterDummy
call s:define_sign_symbols()
call s:define_sign_text_highlights()
call s:define_sign_line_highlights()
endfunction
function! s:define_sign_symbols()
exe "sign define GitGutterLineAdded text=" . g:gitgutter_sign_added
exe "sign define GitGutterLineModified text=" . g:gitgutter_sign_modified
exe "sign define GitGutterLineRemoved text=" . g:gitgutter_sign_removed
exe "sign define GitGutterLineModifiedRemoved text=" . g:gitgutter_sign_modified_removed
endfunction
function! s:define_sign_text_highlights()
sign define GitGutterLineAdded texthl=GitGutterAdd
sign define GitGutterLineModified texthl=GitGutterChange
sign define GitGutterLineRemoved texthl=GitGutterDelete
sign define GitGutterLineModifiedRemoved texthl=GitGutterChangeDelete
endfunction
function! s:define_sign_line_highlights()
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
" }}}
" Diff processing {{{
function! s:run_diff()
let cmd = 'git diff --no-ext-diff --no-color -U0 ' . shellescape(s:file()) .
\ ' | grep -e "^@@ "'
let diff = system(s:command_in_directory_of_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 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\+\w\+='
let matches = matchlist(sign_line, '^\s\+\w\+=\(\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]
let type = 'GitGutterLine' . s:snake_case_to_camel_case(line[1])
call s:add_sign(line_number, 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_file = s:sign_ids[a:file_name]
call add(sign_ids_for_file, a:id)
else
let sign_ids_for_file = [a:id]
endif
let s:sign_ids[a:file_name] = sign_ids_for_file
endfunction
function! s:is_other_sign(line_number)
return index(s:other_signs, a:line_number) == -1 ? 0 : 1
endfunction
function! s:add_dummy_sign()
let last_line = line('$')
exe ":sign place " . s:dummy_sign_id . " line=" . (last_line + 1) . " name=GitGutterDummy file=" . s:file()
endfunction
" }}}
" Public interface {{{
function! GitGutterAll()
let buffer_ids = g:gitgutter_on_bufenter ? s:visible_buffers() : s:buffers()
for buffer_id in buffer_ids
call GitGutter(fnamemodify(bufname(buffer_id), ':p'))
endfor
endfunction
command GitGutterAll call GitGutterAll()
function! GitGutter(file)
call s:set_file(a:file)
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)
if g:gitgutter_sign_column_always
call s:add_dummy_sign()
endif
call s:clear_signs(a:file)
call s:find_other_signs(a:file)
call s:show_signs(a:file, modified_lines)
endif
endfunction
command GitGutter call GitGutter(s:current_file())
function! GitGutterDisable()
let g:gitgutter_enabled = 0
call s:clear_signs(s:file())
endfunction
command GitGutterDisable call GitGutterDisable()
function! GitGutterEnable()
let g:gitgutter_enabled = 1
call GitGutter(s:current_file())
endfunction
command GitGutterEnable call GitGutterEnable()
function! GitGutterToggle()
if g:gitgutter_enabled
call GitGutterDisable()
else
call GitGutterEnable()
endif
endfunction
command GitGutterToggle call GitGutterToggle()
function! GitGutterLineHighlightsDisable()
let s:highlight_lines = 0
call s:define_sign_line_highlights()
endfunction
command GitGutterLineHighlightsDisable call GitGutterLineHighlightsDisable()
function! GitGutterLineHighlightsEnable()
let s:highlight_lines = 1
call s:define_sign_line_highlights()
endfunction
command GitGutterLineHighlightsEnable call GitGutterLineHighlightsEnable()
function! GitGutterLineHighlightsToggle()
let s:highlight_lines = (s:highlight_lines ? 0 : 1)
call s:define_sign_line_highlights()
endfunction
command GitGutterLineHighlightsToggle call GitGutterLineHighlightsToggle()
function! GitGutterNextHunk(count)
if s:is_active()
let current_line = line('.')
let hunk_count = 0
for hunk in s:hunks
if hunk[2] > current_line
let hunk_count += 1
if hunk_count == a:count
execute 'normal! ' . hunk[2] . 'G'
break
endif
endif
endfor
endif
endfunction
command -count=1 GitGutterNextHunk call GitGutterNextHunk(<count>)
function! GitGutterPrevHunk(count)
if s:is_active()
let current_line = line('.')
let hunk_count = 0
for hunk in reverse(copy(s:hunks))
if hunk[2] < current_line
let hunk_count += 1
if hunk_count == a:count
execute 'normal! ' . hunk[2] . 'G'
break
endif
endif
endfor
endif
endfunction
command -count=1 GitGutterPrevHunk call GitGutterPrevHunk(<count>)
" Returns the git-diff hunks for the 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!
if g:gitgutter_on_bufenter
autocmd BufEnter,BufWritePost,FileWritePost * call GitGutter(s:current_file())
else
autocmd BufReadPost,BufWritePost,FileReadPost,FileWritePost * call GitGutter(s:current_file())
endif
if g:gitgutter_all_on_focusgained && !has('gui_win32')
autocmd FocusGained * call GitGutterAll()
endif
augroup END
" }}}
" vim:set et sw=2 fdm=marker: