mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-01-24 06:10:05 +08:00
349 lines
12 KiB
VimL
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)
|