dot_files/vim-plugins/bundle/nerdtree-git-plugin/nerdtree_plugin/git_status.vim
2017-07-09 00:26:06 +03:00

359 lines
11 KiB
VimL

" ============================================================================
" File: git_status.vim
" Description: plugin for NERD Tree that provides git status support
" Maintainer: Xuyuan Pang <xuyuanp at gmail dot com>
" Last Change: 4 Apr 2014
" License: This program is free software. It comes without any warranty,
" to the extent permitted by applicable law. You can redistribute
" it and/or modify it under the terms of the Do What The Fuck You
" Want To Public License, Version 2, as published by Sam Hocevar.
" See http://sam.zoy.org/wtfpl/COPYING for more details.
" ============================================================================
if exists('g:loaded_nerdtree_git_status')
finish
endif
let g:loaded_nerdtree_git_status = 1
if !exists('g:NERDTreeShowGitStatus')
let g:NERDTreeShowGitStatus = 1
endif
if g:NERDTreeShowGitStatus == 0
finish
endif
if !exists('g:NERDTreeMapNextHunk')
let g:NERDTreeMapNextHunk = ']c'
endif
if !exists('g:NERDTreeMapPrevHunk')
let g:NERDTreeMapPrevHunk = '[c'
endif
if !exists('g:NERDTreeUpdateOnWrite')
let g:NERDTreeUpdateOnWrite = 1
endif
if !exists('g:NERDTreeUpdateOnCursorHold')
let g:NERDTreeUpdateOnCursorHold = 1
endif
if !exists('g:NERDTreeShowIgnoredStatus')
let g:NERDTreeShowIgnoredStatus = 0
endif
if !exists('s:NERDTreeIndicatorMap')
let s:NERDTreeIndicatorMap = {
\ 'Modified' : '✹',
\ 'Staged' : '✚',
\ 'Untracked' : '✭',
\ 'Renamed' : '➜',
\ 'Unmerged' : '═',
\ 'Deleted' : '✖',
\ 'Dirty' : '✗',
\ 'Clean' : '✔︎',
\ 'Ignored' : '☒',
\ 'Unknown' : '?'
\ }
endif
function! NERDTreeGitStatusRefreshListener(event)
if !exists('b:NOT_A_GIT_REPOSITORY')
call g:NERDTreeGitStatusRefresh()
endif
let l:path = a:event.subject
let l:flag = g:NERDTreeGetGitStatusPrefix(l:path)
call l:path.flagSet.clearFlags('git')
if l:flag !=# ''
call l:path.flagSet.addFlag('git', l:flag)
endif
endfunction
" FUNCTION: g:NERDTreeGitStatusRefresh() {{{2
" refresh cached git status
function! g:NERDTreeGitStatusRefresh()
let b:NERDTreeCachedGitFileStatus = {}
let b:NERDTreeCachedGitDirtyDir = {}
let b:NOT_A_GIT_REPOSITORY = 1
let l:root = b:NERDTree.root.path.str()
let l:gitcmd = 'git -c color.status=false status -s'
if g:NERDTreeShowIgnoredStatus
let l:gitcmd = l:gitcmd . ' --ignored'
endif
if exists('g:NERDTreeGitStatusIgnoreSubmodules')
let l:gitcmd = l:gitcmd . ' --ignore-submodules'
if g:NERDTreeGitStatusIgnoreSubmodules ==# 'all' || g:NERDTreeGitStatusIgnoreSubmodules ==# 'dirty' || g:NERDTreeGitStatusIgnoreSubmodules ==# 'untracked'
let l:gitcmd = l:gitcmd . '=' . g:NERDTreeGitStatusIgnoreSubmodules
endif
endif
let l:statusesStr = system(l:gitcmd . ' ' . l:root)
let l:statusesSplit = split(l:statusesStr, '\n')
if l:statusesSplit != [] && l:statusesSplit[0] =~# 'fatal:.*'
let l:statusesSplit = []
return
endif
let b:NOT_A_GIT_REPOSITORY = 0
for l:statusLine in l:statusesSplit
" cache git status of files
let l:pathStr = substitute(l:statusLine, '...', '', '')
let l:pathSplit = split(l:pathStr, ' -> ')
if len(l:pathSplit) == 2
call s:NERDTreeCacheDirtyDir(l:pathSplit[0])
let l:pathStr = l:pathSplit[1]
else
let l:pathStr = l:pathSplit[0]
endif
let l:pathStr = s:NERDTreeTrimDoubleQuotes(l:pathStr)
if l:pathStr =~# '\.\./.*'
continue
endif
let l:statusKey = s:NERDTreeGetFileGitStatusKey(l:statusLine[0], l:statusLine[1])
let b:NERDTreeCachedGitFileStatus[fnameescape(l:pathStr)] = l:statusKey
if l:statusKey == 'Ignored'
if isdirectory(l:pathStr)
let b:NERDTreeCachedGitDirtyDir[fnameescape(l:pathStr)] = l:statusKey
endif
else
call s:NERDTreeCacheDirtyDir(l:pathStr)
endif
endfor
endfunction
function! s:NERDTreeCacheDirtyDir(pathStr)
" cache dirty dir
let l:dirtyPath = s:NERDTreeTrimDoubleQuotes(a:pathStr)
if l:dirtyPath =~# '\.\./.*'
return
endif
let l:dirtyPath = substitute(l:dirtyPath, '/[^/]*$', '/', '')
while l:dirtyPath =~# '.\+/.*' && has_key(b:NERDTreeCachedGitDirtyDir, fnameescape(l:dirtyPath)) == 0
let b:NERDTreeCachedGitDirtyDir[fnameescape(l:dirtyPath)] = 'Dirty'
let l:dirtyPath = substitute(l:dirtyPath, '/[^/]*/$', '/', '')
endwhile
endfunction
function! s:NERDTreeTrimDoubleQuotes(pathStr)
let l:toReturn = substitute(a:pathStr, '^"', '', '')
let l:toReturn = substitute(l:toReturn, '"$', '', '')
return l:toReturn
endfunction
" FUNCTION: g:NERDTreeGetGitStatusPrefix(path) {{{2
" return the indicator of the path
" Args: path
let s:GitStatusCacheTimeExpiry = 2
let s:GitStatusCacheTime = 0
function! g:NERDTreeGetGitStatusPrefix(path)
if localtime() - s:GitStatusCacheTime > s:GitStatusCacheTimeExpiry
let s:GitStatusCacheTime = localtime()
call g:NERDTreeGitStatusRefresh()
endif
let l:pathStr = a:path.str()
let l:cwd = b:NERDTree.root.path.str() . a:path.Slash()
if nerdtree#runningWindows()
let l:pathStr = a:path.WinToUnixPath(l:pathStr)
let l:cwd = a:path.WinToUnixPath(l:cwd)
endif
let l:pathStr = substitute(l:pathStr, fnameescape(l:cwd), '', '')
let l:statusKey = ''
if a:path.isDirectory
let l:statusKey = get(b:NERDTreeCachedGitDirtyDir, fnameescape(l:pathStr . '/'), '')
else
let l:statusKey = get(b:NERDTreeCachedGitFileStatus, fnameescape(l:pathStr), '')
endif
return s:NERDTreeGetIndicator(l:statusKey)
endfunction
" FUNCTION: s:NERDTreeGetCWDGitStatus() {{{2
" return the indicator of cwd
function! g:NERDTreeGetCWDGitStatus()
if b:NOT_A_GIT_REPOSITORY
return ''
elseif b:NERDTreeCachedGitDirtyDir == {} && b:NERDTreeCachedGitFileStatus == {}
return s:NERDTreeGetIndicator('Clean')
endif
return s:NERDTreeGetIndicator('Dirty')
endfunction
function! s:NERDTreeGetIndicator(statusKey)
if exists('g:NERDTreeIndicatorMapCustom')
let l:indicator = get(g:NERDTreeIndicatorMapCustom, a:statusKey, '')
if l:indicator !=# ''
return l:indicator
endif
endif
let l:indicator = get(s:NERDTreeIndicatorMap, a:statusKey, '')
if l:indicator !=# ''
return l:indicator
endif
return ''
endfunction
function! s:NERDTreeGetFileGitStatusKey(us, them)
if a:us ==# '?' && a:them ==# '?'
return 'Untracked'
elseif a:us ==# ' ' && a:them ==# 'M'
return 'Modified'
elseif a:us =~# '[MAC]'
return 'Staged'
elseif a:us ==# 'R'
return 'Renamed'
elseif a:us ==# 'U' || a:them ==# 'U' || a:us ==# 'A' && a:them ==# 'A' || a:us ==# 'D' && a:them ==# 'D'
return 'Unmerged'
elseif a:them ==# 'D'
return 'Deleted'
elseif a:us ==# '!'
return 'Ignored'
else
return 'Unknown'
endif
endfunction
" FUNCTION: s:jumpToNextHunk(node) {{{2
function! s:jumpToNextHunk(node)
let l:position = search('\[[^{RO}].*\]', '')
if l:position
call nerdtree#echo('Jump to next hunk ')
endif
endfunction
" FUNCTION: s:jumpToPrevHunk(node) {{{2
function! s:jumpToPrevHunk(node)
let l:position = search('\[[^{RO}].*\]', 'b')
if l:position
call nerdtree#echo('Jump to prev hunk ')
endif
endfunction
" Function: s:SID() {{{2
function s:SID()
if !exists('s:sid')
let s:sid = matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
endif
return s:sid
endfun
" FUNCTION: s:NERDTreeGitStatusKeyMapping {{{2
function! s:NERDTreeGitStatusKeyMapping()
let l:s = '<SNR>' . s:SID() . '_'
call NERDTreeAddKeyMap({
\ 'key': g:NERDTreeMapNextHunk,
\ 'scope': 'Node',
\ 'callback': l:s.'jumpToNextHunk',
\ 'quickhelpText': 'Jump to next git hunk' })
call NERDTreeAddKeyMap({
\ 'key': g:NERDTreeMapPrevHunk,
\ 'scope': 'Node',
\ 'callback': l:s.'jumpToPrevHunk',
\ 'quickhelpText': 'Jump to prev git hunk' })
endfunction
augroup nerdtreegitplugin
autocmd CursorHold * silent! call s:CursorHoldUpdate()
augroup END
" FUNCTION: s:CursorHoldUpdate() {{{2
function! s:CursorHoldUpdate()
if g:NERDTreeUpdateOnCursorHold != 1
return
endif
if !g:NERDTree.IsOpen()
return
endif
" Do not update when a special buffer is selected
if !empty(&l:buftype)
return
endif
let l:winnr = winnr()
let l:altwinnr = winnr('#')
call g:NERDTree.CursorToTreeWin()
call b:NERDTree.root.refreshFlags()
call NERDTreeRender()
exec l:altwinnr . 'wincmd w'
exec l:winnr . 'wincmd w'
endfunction
augroup nerdtreegitplugin
autocmd BufWritePost * call s:FileUpdate(expand('%:p'))
augroup END
" FUNCTION: s:FileUpdate(fname) {{{2
function! s:FileUpdate(fname)
if g:NERDTreeUpdateOnWrite != 1
return
endif
if !g:NERDTree.IsOpen()
return
endif
let l:winnr = winnr()
let l:altwinnr = winnr('#')
call g:NERDTree.CursorToTreeWin()
let l:node = b:NERDTree.root.findNode(g:NERDTreePath.New(a:fname))
if l:node == {}
return
endif
call l:node.refreshFlags()
let l:node = l:node.parent
while !empty(l:node)
call l:node.refreshDirFlags()
let l:node = l:node.parent
endwhile
call NERDTreeRender()
exec l:altwinnr . 'wincmd w'
exec l:winnr . 'wincmd w'
endfunction
augroup AddHighlighting
autocmd FileType nerdtree call s:AddHighlighting()
augroup END
function! s:AddHighlighting()
let l:synmap = {
\ 'NERDTreeGitStatusModified' : s:NERDTreeGetIndicator('Modified'),
\ 'NERDTreeGitStatusStaged' : s:NERDTreeGetIndicator('Staged'),
\ 'NERDTreeGitStatusUntracked' : s:NERDTreeGetIndicator('Untracked'),
\ 'NERDTreeGitStatusRenamed' : s:NERDTreeGetIndicator('Renamed'),
\ 'NERDTreeGitStatusIgnored' : s:NERDTreeGetIndicator('Ignored'),
\ 'NERDTreeGitStatusDirDirty' : s:NERDTreeGetIndicator('Dirty'),
\ 'NERDTreeGitStatusDirClean' : s:NERDTreeGetIndicator('Clean')
\ }
for l:name in keys(l:synmap)
exec 'syn match ' . l:name . ' #' . escape(l:synmap[l:name], '~') . '# containedin=NERDTreeFlags'
endfor
hi def link NERDTreeGitStatusModified Special
hi def link NERDTreeGitStatusStaged Function
hi def link NERDTreeGitStatusRenamed Title
hi def link NERDTreeGitStatusUnmerged Label
hi def link NERDTreeGitStatusUntracked Comment
hi def link NERDTreeGitStatusDirDirty Tag
hi def link NERDTreeGitStatusDirClean DiffAdd
" TODO: use diff color
hi def link NERDTreeGitStatusIgnored DiffAdd
endfunction
function! s:SetupListeners()
call g:NERDTreePathNotifier.AddListener('init', 'NERDTreeGitStatusRefreshListener')
call g:NERDTreePathNotifier.AddListener('refresh', 'NERDTreeGitStatusRefreshListener')
call g:NERDTreePathNotifier.AddListener('refreshFlags', 'NERDTreeGitStatusRefreshListener')
endfunction
if g:NERDTreeShowGitStatus && executable('git')
call s:NERDTreeGitStatusKeyMapping()
call s:SetupListeners()
endif