" ============================================================================ " File: git_status.vim " Description: plugin for NERD Tree that provides git status support " Maintainer: Xuyuan Pang " 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. " ============================================================================ scriptencoding utf-8 if exists('g:loaded_nerdtree_git_status') finish endif let g:loaded_nerdtree_git_status = 1 let s:is_win = gitstatus#isWin() " stolen from nerdtree "Function: s:initVariable() function {{{2 "This function is used to initialise a given variable to a given value. The "variable is only initialised if it does not exist prior " "Args: "var: the name of the var to be initialised "value: the value to initialise var to " "Returns: "1 if the var is set, 0 otherwise function! s:initVariable(var, value) abort if !exists(a:var) exec 'let ' . a:var . ' = ' . "'" . substitute(a:value, "'", "''", 'g') . "'" return 1 endif return 0 endfunction let s:default_vals = { \ 'g:NERDTreeGitStatusEnable': 1, \ 'g:NERDTreeGitStatusUpdateOnWrite': 1, \ 'g:NERDTreeGitStatusUpdateOnCursorHold': 1, \ 'g:NERDTreeGitStatusShowIgnored': 0, \ 'g:NERDTreeGitStatusUseNerdFonts': 0, \ 'g:NERDTreeGitStatusDirDirtyOnly': 1, \ 'g:NERDTreeGitStatusConcealBrackets': 0, \ 'g:NERDTreeGitStatusAlignIfConceal': 1, \ 'g:NERDTreeGitStatusShowClean': 0, \ 'g:NERDTreeGitStatusLogLevel': 2, \ 'g:NERDTreeGitStatusPorcelainVersion': 2, \ 'g:NERDTreeGitStatusCwdOnly': 0, \ 'g:NERDTreeGitStatusMapNextHunk': ']c', \ 'g:NERDTreeGitStatusMapPrevHunk': '[c', \ 'g:NERDTreeGitStatusUntrackedFilesMode': 'normal', \ 'g:NERDTreeGitStatusGitBinPath': 'git', \ } for [s:var, s:value] in items(s:default_vals) call s:initVariable(s:var, s:value) endfor let s:logger = gitstatus#log#NewLogger(g:NERDTreeGitStatusLogLevel) function! s:deprecated(oldv, newv) abort call s:logger.warning(printf("option '%s' is deprecated, please use '%s'", a:oldv, a:newv)) endfunction function! s:migrateVariable(oldv, newv) abort if exists(a:oldv) call s:deprecated(a:oldv, a:newv) exec 'let ' . a:newv . ' = ' . a:oldv return 1 endif return 0 endfunction let s:need_migrate_vals = { \ 'g:NERDTreeShowGitStatus': 'g:NERDTreeGitStatusEnable', \ 'g:NERDTreeUpdateOnWrite': 'g:NERDTreeGitStatusUpdateOnWrite', \ 'g:NERDTreeMapNextHunk': 'g:NERDTreeGitStatusMapNextHunk', \ 'g:NERDTreeMapPrevHunk': 'g:NERDTreeGitStatusMapPrevHunk', \ 'g:NERDTreeShowIgnoredStatus': 'g:NERDTreeGitStatusShowIgnored', \ 'g:NERDTreeIndicatorMapCustom': 'g:NERDTreeGitStatusIndicatorMapCustom', \ } for [s:oldv, s:newv] in items(s:need_migrate_vals) call s:migrateVariable(s:oldv, s:newv) endfor if !g:NERDTreeGitStatusEnable finish endif if !executable(g:NERDTreeGitStatusGitBinPath) call s:logger.error('git command not found') finish endif " FUNCTION: path2str " This function is used to format nerdtree.Path. " For Windows, returns in format 'C:/path/to/file' " " ARGS: " path: nerdtree.Path " " RETURNS: " absolute path function! s:path2str(path) abort return gitstatus#util#FormatPath(a:path) endfunction " disable ProhibitUnusedVariable because these three functions used to callback " vint: -ProhibitUnusedVariable function! s:onGitWorkdirSuccessCB(job) abort let g:NTGitWorkdir = split(join(a:job.chunks, ''), "\n")[0] call s:logger.debug(printf("'%s' is in a git repo: '%s'", a:job.opts.cwd, g:NTGitWorkdir)) call s:enableLiveUpdate() call s:refreshGitStatus('init', g:NTGitWorkdir) endfunction function! s:onGitWorkdirFailedCB(job) abort let l:errormsg = join(a:job.err_chunks, '') if l:errormsg =~# 'fatal: Not a git repository' call s:logger.debug(printf("'%s' is not in a git repo", a:job.opts.cwd)) endif call s:disableLiveUpdate() unlet! g:NTGitWorkdir endfunction function! s:getGitWorkdir(ntRoot) abort call gitstatus#job#Spawn('git-workdir', \ s:buildGitWorkdirCommand(a:ntRoot), \ { \ 'on_success_cb': function('s:onGitWorkdirSuccessCB'), \ 'on_failed_cb': function('s:onGitWorkdirFailedCB'), \ 'cwd': a:ntRoot, \ }) endfunction " vint: +ProhibitUnusedVariable function! s:buildGitWorkdirCommand(root) abort return gitstatus#util#BuildGitWorkdirCommand(a:root, g:) endfunction function! s:buildGitStatusCommand(workdir) abort return gitstatus#util#BuildGitStatusCommand(a:workdir, g:) endfunction function! s:refreshGitStatus(name, workdir) abort let l:opts = { \ 'on_failed_cb': function('s:onGitStatusFailedCB'), \ 'on_success_cb': function('s:onGitStatusSuccessCB'), \ 'cwd': a:workdir \ } let l:job = gitstatus#job#Spawn(a:name, s:buildGitStatusCommand(a:workdir), l:opts) return l:job endfunction " vint: -ProhibitUnusedVariable function! s:onGitStatusSuccessCB(job) abort if !exists('g:NTGitWorkdir') || g:NTGitWorkdir !=# a:job.opts.cwd call s:logger.debug(printf("git workdir has changed: '%s' -> '%s'", a:job.opts.cwd, get(g:, 'NTGitWorkdir', ''))) return endif let l:output = join(a:job.chunks, '') let l:lines = split(l:output, "\n") let l:cache = gitstatus#util#ParseGitStatusLines(a:job.opts.cwd, l:lines, g:) try call s:listener.SetNext(l:cache) call s:listener.TryUpdateNERDTreeUI() catch endtry endfunction function! s:onGitStatusFailedCB(job) abort let l:errormsg = join(a:job.err_chunks, '') if l:errormsg =~# "error: option `porcelain' takes no value" call s:logger.error(printf("'git status' command failed, please upgrade your git binary('v2.11.0' or higher) or set option 'g:NERDTreeGitStatusPorcelainVersion' to 1 in vimrc")) call s:disableLiveUpdate() unlet! g:NTGitWorkdir elseif l:errormsg =~# '^warning: could not open .* Permission denied' call s:onGitStatusSuccessCB(a:job) else call s:logger.error(printf('job[%s] failed: %s', a:job.name, l:errormsg)) endif endfunction " FUNCTION: s:onCursorHold(fname) {{{2 function! s:onCursorHold(fname) " Do not update when a special buffer is selected if !empty(&l:buftype) return endif let l:fname = s:is_win ? \ substitute(a:fname, '\', '/', 'g') : \ a:fname if !exists('g:NTGitWorkdir') || !s:hasPrefix(l:fname, g:NTGitWorkdir) return endif let l:job = s:refreshGitStatus('cursor-hold', g:NTGitWorkdir) call s:logger.debug('run cursor-hold job: ' . l:job.id) endfunction " FUNCTION: s:onFileUpdate(fname) {{{2 function! s:onFileUpdate(fname) let l:fname = s:is_win ? \ substitute(a:fname, '\', '/', 'g') : \ a:fname if !exists('g:NTGitWorkdir') || !s:hasPrefix(l:fname, g:NTGitWorkdir) return endif let l:job = s:refreshGitStatus('file-update', g:NTGitWorkdir) call s:logger.debug('run file-update job: ' . l:job.id) endfunction " vint: +ProhibitUnusedVariable function! s:hasPrefix(text, prefix) abort return len(a:text) >= len(a:prefix) && a:text[:len(a:prefix)-1] ==# a:prefix endfunction function! s:setupNERDTreeListeners(listener) abort call g:NERDTreePathNotifier.AddListener('init', a:listener.OnInit) call g:NERDTreePathNotifier.AddListener('refresh', a:listener.OnRefresh) call g:NERDTreePathNotifier.AddListener('refreshFlags', a:listener.OnRefreshFlags) endfunction " FUNCTION: s:findHunk(node, direction) " Args: " node: the current node " direction: next(>0) or prev(<0) " " Returns: " lineNum if the hunk found, -1 otherwise function! s:findHunk(node, direction) abort let l:ui = b:NERDTree.ui let l:rootLn = l:ui.getRootLineNum() let l:totalLn = line('$') let l:currLn = l:ui.getLineNum(a:node) let l:currLn = l:currLn <= l:rootLn ? l:rootLn+1 : l:currLn let l:step = a:direction > 0 ? 1 : -1 let l:lines = a:direction > 0 ? \ range(l:currLn+1, l:totalLn, l:step) + range(l:rootLn+1, l:currLn-1, l:step) : \ range(l:currLn-1, l:rootLn+1, l:step) + range(l:totalLn, l:currLn+1, l:step) for l:ln in l:lines let l:path = s:path2str(l:ui.getPath(l:ln)) if s:listener.HasPath(l:path) return l:ln endif endfor return -1 endfunction " vint: -ProhibitUnusedVariable " FUNCTION: s:jumpToNextHunk(node) {{{2 function! s:jumpToNextHunk(node) let l:ln = s:findHunk(a:node, 1) if l:ln > 0 exec '' . l:ln call s:logger.info('Jump to next hunk') endif endfunction " FUNCTION: s:jumpToPrevHunk(node) {{{2 function! s:jumpToPrevHunk(node) let l:ln = s:findHunk(a:node, -1) if l:ln > 0 exec '' . l:ln call s:logger.info('Jump to prev hunk') endif endfunction " vint: +ProhibitUnusedVariable " 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:setupNERDTreeKeyMappings {{{2 function! s:setupNERDTreeKeyMappings() let l:s = '' . s:SID() . '_' call NERDTreeAddKeyMap({ \ 'key': g:NERDTreeGitStatusMapNextHunk, \ 'scope': 'Node', \ 'callback': l:s.'jumpToNextHunk', \ 'quickhelpText': 'Jump to next git hunk' }) call NERDTreeAddKeyMap({ \ 'key': g:NERDTreeGitStatusMapPrevHunk, \ 'scope': 'Node', \ 'callback': l:s.'jumpToPrevHunk', \ 'quickhelpText': 'Jump to prev git hunk' }) endfunction " I don't know why, but vint said they are unused. " vint: -ProhibitUnusedVariable function! s:onNERDTreeDirChanged(path) abort call s:getGitWorkdir(a:path) endfunction function! s:onNERDTreeInit(path) abort call s:getGitWorkdir(a:path) endfunction " vint: +ProhibitUnusedVariable function! s:enableLiveUpdate() abort augroup nerdtreegitplugin_liveupdate autocmd! if g:NERDTreeGitStatusUpdateOnWrite autocmd BufWritePost * silent! call s:onFileUpdate(expand('%:p')) endif if g:NERDTreeGitStatusUpdateOnCursorHold autocmd CursorHold * silent! call s:onCursorHold(expand('%:p')) endif " TODO: is it necessary to pass the buffer name? autocmd User FugitiveChanged silent! call s:onFileUpdate(expand('%:p')) autocmd BufEnter NERD_tree_* if exists('b:NERDTree') | \ call s:onNERDTreeInit(s:path2str(b:NERDTree.root.path)) | endif augroup end endfunction function! s:disableLiveUpdate() abort augroup nerdtreegitplugin_liveupdate autocmd! augroup end endfunction augroup nerdtreegitplugin autocmd! autocmd User NERDTreeInit call s:onNERDTreeInit(s:path2str(b:NERDTree.root.path)) autocmd User NERDTreeNewRoot call s:onNERDTreeDirChanged(s:path2str(b:NERDTree.root.path)) augroup end call s:setupNERDTreeKeyMappings() let s:listener = gitstatus#listener#New(g:) call s:setupNERDTreeListeners(s:listener)