" ============================================================================ " File: git_status.vim " Description: plugin for NERD Tree that provides git status support " Maintainer: Xuyuan Pang " 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 = fnamemodify(b:NERDTree.root.path.str(), ':p:gs?\\?/?:S') let l:gitcmd = 'git -c color.status=false -C ' . l:root . ' 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) 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:cwd = substitute(l:cwd, '\~', '\\~', 'g') let l:pathStr = substitute(l:pathStr, 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(''), '\zs\d\+\ze_SID$') endif return s:sid endfun " FUNCTION: s:NERDTreeGitStatusKeyMapping {{{2 function! s:NERDTreeGitStatusKeyMapping() let l:s = '' . 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