1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 06:30:03 +08:00
SpaceVim/bundle/nerdtree-git-plugin/nerdtree_plugin/git_status.vim
2021-09-12 23:45:01 +08:00

349 lines
12 KiB
VimL

" ============================================================================
" File: git_status.vim
" Description: plugin for NERD Tree that provides git status support
" Maintainer: Xuyuan Pang <xuyuanp at gmail dot com>
" 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('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
endif
return s:sid
endfun
" FUNCTION: s:setupNERDTreeKeyMappings {{{2
function! s:setupNERDTreeKeyMappings()
let l:s = '<SNR>' . 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)