├── tests ├── .themisrc └── test_all.vim ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── question.md │ └── bug.md ├── stale.yml └── workflows │ └── ci.yml ├── .pre-commit-config.yaml ├── LICENSE ├── .vintrc.yaml ├── autoload ├── gitstatus │ ├── log.vim │ ├── listener.vim │ ├── job.vim │ ├── doctor.vim │ └── util.vim └── gitstatus.vim ├── after └── syntax │ └── nerdtree.vim ├── README.md └── nerdtree_plugin └── git_status.vim /tests/.themisrc: -------------------------------------------------------------------------------- 1 | if exists('$THEMIS_PROFILE') 2 | execute 'profile' 'start' $THEMIS_PROFILE 3 | profile! file ./autoload/* 4 | endif 5 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description of Changes 2 | Closes # 3 | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature Request" 3 | about: "What new feature are you requesting for nerdtree-git-plugin?" 4 | labels: "feature request" 5 | --- 6 | 7 | #### Description 8 | 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-added-large-files 10 | - id: mixed-line-ending 11 | - repo: https://github.com/Vimjas/vint 12 | rev: v0.4a3 13 | hooks: 14 | - id: vint 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: stale 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.vintrc.yaml: -------------------------------------------------------------------------------- 1 | cmdargs: 2 | # Checking more strictly 3 | severity: style_problem 4 | 5 | # Enable coloring 6 | color: false 7 | 8 | # Enable Neovim syntax 9 | env: 10 | neovim: true 11 | 12 | policies: 13 | ProhibitAutocmdWithNoGroup: 14 | enabled: true 15 | ProhibitCommandRelyOnUser: 16 | enabled: true 17 | ProhibitCommandWithUnintendedSideEffect: 18 | enabled: true 19 | ProhibitEncodingOptionAfterScriptEncoding: 20 | enabled: true 21 | ProhibitEqualTildeOperator: 22 | enabled: true 23 | ProhibitImplicitScopeBuiltinVariable: 24 | enabled: true 25 | ProhibitMissingScriptEncoding: 26 | enabled: true 27 | ProhibitNoAbortFunction: 28 | enabled: true 29 | ProhibitSetNoCompatible: 30 | enabled: true 31 | ProhibitUnnecessaryDoubleQuote: 32 | enabled: true 33 | ProhibitUnusedVariable: 34 | enabled: true 35 | ProhibitUsingUndeclaredVariable: 36 | enabled: true 37 | ProhibitAbbreviationOption: 38 | enabled: true 39 | ProhibitImplicitScopeVariable: 40 | enabled: true 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "General Question" 3 | about: "Having trouble setting up nerdtree-git-plugin? Need clarification on a setting? Ask your question here." 4 | labels: "general question" 5 | --- 6 | 15 | 16 | #### Self-Diagnosis 17 | 18 | - [ ] I have searched the [issues](https://github.com/Xuyuanp/nerdtree-git-plugin/issues) for an answer to my question. 19 | - [ ] I have reviewed the NERDTree documentation(README.md). 20 | - [ ] I have searched the web for an answer to my question. 21 | 22 | #### State Your Question 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug Report" 3 | about: "nerdtree-git-plugin is misbehaving? Tell us about it." 4 | labels: bug 5 | --- 6 | 15 | 16 | #### Self-Diagnosis 17 | 18 | - [ ] I have searched the [issues](https://github.com/Xuyuanp/nerdtree-git-plugin/issues) for an answer to my question. 19 | - [ ] I have reviewed the NERDTree documentation(README.md). 20 | - [ ] I have searched the web for an answer to my question. 21 | 22 | #### Environment (for bug reports) 23 | - [ ] Operating System: 24 | - [ ] vimrc settings 25 | ```vim 26 | " all settings about nerdtree and other plugins 27 | ``` 28 | - Other NERDTree-dependent Plugins 29 | - [ ] jistr/vim-nerdtree-tabs 30 | - [ ] ryanoasis/vim-devicons 31 | - [ ] tiagofumo/vim-nerdtree-syntax-highlight 32 | - [ ] Others (specify): 33 | - [ ] I've verified the issue occurs with only `nerdtree-git-plugin` installed. 34 | - [ ] Copy-Paste `call gitstatus#doctor#Say()` outputs 35 | 36 | #### Steps to Reproduce the Issue 37 | 1. 38 | 39 | #### Current Result (Include screenshots where appropriate.) 40 | 41 | #### Expected Result 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | vint: 5 | name: Vint 6 | strategy: 7 | fail-fast: false 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: Run vint with reviewdog 13 | uses: reviewdog/action-vint@v1.0.1 14 | with: 15 | github_token: ${{ secrets.github_token }} 16 | reporter: github-pr-review 17 | test: 18 | name: Unit tests 19 | strategy: 20 | matrix: 21 | os: [macos-latest, ubuntu-latest, windows-latest] 22 | neovim: [true, false] 23 | version: [stable, nightly] 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v2 28 | - name: Checkout themis.vim 29 | uses: actions/checkout@v2 30 | with: 31 | repository: thinca/vim-themis 32 | path: tmp/vim-themis 33 | - name: Checkout nerdtree 34 | uses: actions/checkout@v2 35 | with: 36 | repository: preservim/nerdtree 37 | path: tmp/nerdtree 38 | # Remove apt repos that are known to break from time to time 39 | # See https://github.com/actions/virtual-environments/issues/323 40 | - name: Remove broken apt repos [Ubuntu] 41 | if: matrix.os == 'ubuntu-latest' 42 | run: | 43 | for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done 44 | - name: Install Vim or Neovim 45 | uses: rhysd/action-setup-vim@v1 46 | id: vim 47 | with: 48 | neovim: ${{ matrix.neovim }} 49 | version: ${{ matrix.version }} 50 | - name: Run unit tests 51 | env: 52 | THEMIS_VIM: ${{ steps.vim.outputs.executable }} 53 | THEMIS_PROFILE: profile.txt 54 | run: | 55 | echo $THEMIS_VIM 56 | ./tmp/vim-themis/bin/themis --runtimepath ./tmp/nerdtree --reporter spec tests/ 57 | # TODO: coverage 58 | -------------------------------------------------------------------------------- /autoload/gitstatus/log.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " File: autoload/gitstatus/job.vim 3 | " Description: leveled-logger 4 | " Maintainer: Xuyuan Pang 5 | " License: This program is free software. It comes without any warranty, 6 | " to the extent permitted by applicable law. You can redistribute 7 | " it and/or modify it under the terms of the Do What The Fuck You 8 | " Want To Public License, Version 2, as published by Sam Hocevar. 9 | " See http://sam.zoy.org/wtfpl/COPYING for more details. 10 | " ============================================================================ 11 | if exists('g:loaded_nerdtree_git_status_log') 12 | finish 13 | endif 14 | let g:loaded_nerdtree_git_status_log = 1 15 | 16 | let s:debug = 0 | :lockvar s:debug 17 | let s:info = 1 | :lockvar s:info 18 | let s:warning = 2 | :lockvar s:warning 19 | let s:error = 3 | :lockvar s:error 20 | 21 | let s:Logger = {} 22 | 23 | " vint: -ProhibitImplicitScopeVariable 24 | function! s:Logger.output(level, msg) abort 25 | if a:level < self.level 26 | return 27 | endif 28 | echomsg '[nerdtree-git-status] ' . a:msg 29 | endfunction 30 | 31 | function! s:Logger.debug(msg) abort 32 | echohl LineNr | 33 | \ call self.output(s:debug, a:msg) | 34 | \ echohl None 35 | endfunction 36 | 37 | function! s:Logger.info(msg) abort 38 | call self.output(s:info, a:msg) 39 | endfunction 40 | 41 | function! s:Logger.warning(msg) abort 42 | echohl WarningMsg | 43 | \ call self.output(s:warning, a:msg) | 44 | \ echohl None 45 | endfunction 46 | 47 | function! s:Logger.error(msg) abort 48 | echohl ErrorMsg | 49 | \ call self.output(s:error, a:msg) | 50 | \ echohl None 51 | endfunction 52 | " vint: +ProhibitImplicitScopeVariable 53 | 54 | function! gitstatus#log#NewLogger(level) abort 55 | return extend(copy(s:Logger), {'level': a:level}) 56 | endfunction 57 | -------------------------------------------------------------------------------- /after/syntax/nerdtree.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " File: autoload/gitstatus/job.vim 3 | " Description: git status indicator syntax highlighting 4 | " Maintainer: Xuyuan Pang 5 | " License: This program is free software. It comes without any warranty, 6 | " to the extent permitted by applicable law. You can redistribute 7 | " it and/or modify it under the terms of the Do What The Fuck You 8 | " Want To Public License, Version 2, as published by Sam Hocevar. 9 | " See http://sam.zoy.org/wtfpl/COPYING for more details. 10 | " ============================================================================ 11 | if !get(g:, 'NERDTreeGitStatusEnable', 0) 12 | finish 13 | endif 14 | 15 | function! s:getIndicator(status) abort 16 | return gitstatus#getIndicator(a:status) 17 | endfunction 18 | 19 | if gitstatus#shouldConceal() 20 | " Hide the backets 21 | syntax match hideBracketsInNerdTreeL "\]" contained conceal containedin=NERDTreeFlags 22 | syntax match hideBracketsInNerdTreeR "\[" contained conceal containedin=NERDTreeFlags 23 | setlocal conceallevel=3 24 | setlocal concealcursor=nvic 25 | endif 26 | 27 | function! s:highlightFromGroup(group) abort 28 | let l:synid = synIDtrans(hlID(a:group)) 29 | let [l:ctermfg, l:guifg] = [synIDattr(l:synid, 'fg', 'cterm'), synIDattr(l:synid, 'fg', 'gui')] 30 | return 'cterm=NONE ctermfg=' . l:ctermfg . ' ctermbg=NONE gui=NONE guifg=' . l:guifg . ' guibg=NONE' 31 | endfunction 32 | 33 | function! s:setHightlighting() abort 34 | let l:synlist = [ 35 | \ ['Unmerged', 'Function'], 36 | \ ['Modified', 'Special'], 37 | \ ['Staged', 'Function'], 38 | \ ['Renamed', 'Title'], 39 | \ ['Unmerged', 'Label'], 40 | \ ['Untracked', 'Comment'], 41 | \ ['Dirty', 'Tag'], 42 | \ ['Deleted', 'Operator'], 43 | \ ['Ignored', 'SpecialKey'], 44 | \ ['Clean', 'Method'], 45 | \ ] 46 | 47 | for [l:name, l:group] in l:synlist 48 | let l:indicator = escape(s:getIndicator(l:name), '\#-*.$') 49 | let l:synname = 'NERDTreeGitStatus' . l:name 50 | execute 'silent! syntax match ' . l:synname . ' #\m\C\zs[' . l:indicator . ']\ze[^\]]*\]# containedin=NERDTreeFlags' 51 | let l:hipat = get(get(g:, 'NERDTreeGitStatusHighlightingCustom', {}), 52 | \ l:name, 53 | \ s:highlightFromGroup(l:group)) 54 | execute 'silent! highlight ' . l:synname . ' ' . l:hipat 55 | endfor 56 | endfunction 57 | 58 | silent! call s:setHightlighting() 59 | -------------------------------------------------------------------------------- /autoload/gitstatus.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " File: autoload/gitstatus.vim 3 | " Description: library for indicators 4 | " Maintainer: Xuyuan Pang 5 | " License: This program is free software. It comes without any warranty, 6 | " to the extent permitted by applicable law. You can redistribute 7 | " it and/or modify it under the terms of the Do What The Fuck You 8 | " Want To Public License, Version 2, as published by Sam Hocevar. 9 | " See http://sam.zoy.org/wtfpl/COPYING for more details. 10 | " ============================================================================ 11 | if exists('g:loaded_nerdtree_git_status_autoload') 12 | finish 13 | endif 14 | let g:loaded_nerdtree_git_status_autoload = 1 15 | 16 | function! gitstatus#isWin() abort 17 | return has('win16') || has('win32') || has('win64') 18 | endfunction 19 | 20 | if get(g:, 'NERDTreeGitStatusUseNerdFonts', 0) 21 | let s:indicatorMap = { 22 | \ 'Modified' :nr2char(61545), 23 | \ 'Staged' :nr2char(61543), 24 | \ 'Untracked' :nr2char(61736), 25 | \ 'Renamed' :nr2char(62804), 26 | \ 'Unmerged' :nr2char(61556), 27 | \ 'Deleted' :nr2char(63167), 28 | \ 'Dirty' :nr2char(61453), 29 | \ 'Ignored' :nr2char(61738), 30 | \ 'Clean' :nr2char(61452), 31 | \ 'Unknown' :nr2char(61832) 32 | \ } 33 | elseif &encoding ==? 'utf-8' 34 | let s:indicatorMap = { 35 | \ 'Modified' :nr2char(10041), 36 | \ 'Staged' :nr2char(10010), 37 | \ 'Untracked' :nr2char(10029), 38 | \ 'Renamed' :nr2char(10140), 39 | \ 'Unmerged' :nr2char(9552), 40 | \ 'Deleted' :nr2char(10006), 41 | \ 'Dirty' :nr2char(10007), 42 | \ 'Ignored' :nr2char(33), 43 | \ 'Clean' :nr2char(10004), 44 | \ 'Unknown' :nr2char(120744) 45 | \ } 46 | else 47 | let s:indicatorMap = { 48 | \ 'Modified' :'*', 49 | \ 'Staged' :'+', 50 | \ 'Untracked' :'!', 51 | \ 'Renamed' :'R', 52 | \ 'Unmerged' :'=', 53 | \ 'Deleted' :'D', 54 | \ 'Dirty' :'X', 55 | \ 'Ignored' :'?', 56 | \ 'Clean' :'C', 57 | \ 'Unknown' :'E' 58 | \ } 59 | endif 60 | 61 | function! gitstatus#getIndicator(status) abort 62 | return get(get(g:, 'NERDTreeGitStatusIndicatorMapCustom', {}), 63 | \ a:status, 64 | \ s:indicatorMap[a:status]) 65 | endfunction 66 | 67 | function! gitstatus#shouldConceal() abort 68 | return has('conceal') && g:NERDTreeGitStatusConcealBrackets 69 | endfunction 70 | -------------------------------------------------------------------------------- /autoload/gitstatus/listener.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " File: autoload/gitstatus/listener.vim 3 | " Description: nerdtree event listener 4 | " Maintainer: Xuyuan Pang 5 | " License: This program is free software. It comes without any warranty, 6 | " to the extent permitted by applicable law. You can redistribute 7 | " it and/or modify it under the terms of the Do What The Fuck You 8 | " Want To Public License, Version 2, as published by Sam Hocevar. 9 | " See http://sam.zoy.org/wtfpl/COPYING for more details. 10 | " ============================================================================ 11 | if exists('g:loaded_nerdtree_git_status_listener') 12 | finish 13 | endif 14 | let g:loaded_nerdtree_git_status_listener = 1 15 | 16 | let s:Listener = { 17 | \ 'current': {}, 18 | \ 'next': {}, 19 | \ } 20 | 21 | " disabled ProhibitImplicitScopeVariable because we will use lots of `self` 22 | " vint: -ProhibitImplicitScopeVariable 23 | function! s:Listener.OnInit(event) abort 24 | call self.callback(a:event) 25 | endfunction 26 | 27 | function! s:Listener.OnRefresh(event) abort 28 | call self.callback(a:event) 29 | endfunction 30 | 31 | function! s:Listener.OnRefreshFlags(event) abort 32 | call self.callback(a:event) 33 | endfunction 34 | 35 | function! s:Listener.callback(event) abort 36 | let l:path = a:event.subject 37 | let l:indicator = self.getIndicatorByPath(l:path) 38 | call l:path.flagSet.clearFlags('git') 39 | if l:indicator !=# '' 40 | if gitstatus#shouldConceal() 41 | let l:indicator = printf(' %s ', l:indicator) 42 | endif 43 | call l:path.flagSet.addFlag('git', l:indicator) 44 | endif 45 | endfunction 46 | 47 | function!s:Listener.getIndicatorByPath(path) abort 48 | let l:pathStr = gitstatus#util#FormatPath(a:path) 49 | let l:statusKey = get(self.current, l:pathStr, '') 50 | 51 | if l:statusKey !=# '' 52 | return gitstatus#getIndicator(l:statusKey) 53 | endif 54 | 55 | if self.getOption('ShowClean', 0) 56 | return gitstatus#getIndicator('Clean') 57 | endif 58 | 59 | if self.getOption('ConcealBrackets', 0) && self.getOption('AlignIfConceal', 0) 60 | return ' ' 61 | endif 62 | return '' 63 | endfunction 64 | 65 | function! s:Listener.SetNext(cache) abort 66 | let self.next = a:cache 67 | endfunction 68 | 69 | function! s:Listener.HasPath(path_str) abort 70 | return has_key(self.current, a:path_str) 71 | endfunction 72 | 73 | function! s:Listener.changed() abort 74 | return self.current !=# self.next 75 | endfunction 76 | 77 | function! s:Listener.update() abort 78 | let self.current = self.next 79 | endfunction 80 | 81 | function! s:Listener.TryUpdateNERDTreeUI() abort 82 | if !g:NERDTree.IsOpen() 83 | return 84 | endif 85 | 86 | if !self.changed() 87 | return 88 | endif 89 | 90 | call self.update() 91 | 92 | let l:winnr = winnr() 93 | let l:altwinnr = winnr('#') 94 | 95 | try 96 | call g:NERDTree.CursorToTreeWin() 97 | call b:NERDTree.root.refreshFlags() 98 | call NERDTreeRender() 99 | finally 100 | noautocmd exec l:altwinnr . 'wincmd w' 101 | noautocmd exec l:winnr . 'wincmd w' 102 | endtry 103 | endfunction 104 | 105 | function! s:Listener.getOption(name, default) abort 106 | return get(self.opts, 'NERDTreeGitStatus' . a:name, a:default) 107 | endfunction 108 | " vint: +ProhibitImplicitScopeVariable 109 | 110 | function! gitstatus#listener#New(opts) abort 111 | return extend(deepcopy(s:Listener), {'opts': a:opts}) 112 | endfunction 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nerdtree-git-plugin 2 | =================== 3 | [![Github Action](https://img.shields.io/github/workflow/status/Xuyuanp/nerdtree-git-plugin/CI)](https://github.com/Xuyuanp/nerdtree-git-plugin/actions?query=workflow%3ACI) 4 | [![License: WTFPL](https://img.shields.io/github/license/Xuyuanp/nerdtree-git-plugin)](http://www.wtfpl.net/about/) 5 | [![GitHub contributors](https://img.shields.io/github/contributors/Xuyuanp/nerdtree-git-plugin)](https://github.com/Xuyuanp/nerdtree-git-plugin/graphs/contributors) 6 | 7 | A plugin of [NERDTree](https://github.com/preservim/nerdtree) showing git status flags. 8 | 9 | The original project [git-nerdtree](https://github.com/Xuyuanp/git-nerdtree) will not be maintained any longer. 10 | 11 | ![Imgur](http://i.imgur.com/jSCwGjU.gif?1) 12 | 13 | ## Installation 14 | 15 | Use your favorite package manager. Here is the example of using [vim-plug](https://github.com/junegunn/vim-plug) 16 | 17 | ```vim script 18 | Plug 'preservim/nerdtree' | 19 | \ Plug 'Xuyuanp/nerdtree-git-plugin' 20 | ``` 21 | 22 | ## New project 23 | 24 | [Yanil](https://github.com/Xuyuanp/yanil): Another nerdtree like plugin for neovim(>= 0.5.0) only. I'm focusing on this project. 25 | 26 | ## FAQ 27 | 28 | > Got error message like `Error detected while processing function 29 | 177[2]..178[22]..181[7]..144[9]..142[36]..238[4]..NERDTreeGitStatusRefreshListener[2]..NERDTreeGitStatusRefresh: 30 | line 6: 31 | E484: Can't open file /tmp/vZEZ6gM/1` while nerdtree opening in fish, how to resolve this problem? 32 | 33 | This was because that vim couldn't execute `system` function in `fish`. Add `set shell=sh` in your vimrc. 34 | 35 | This issue has been fixed. 36 | 37 | > How to config custom symbols? 38 | 39 | Use this variable to change symbols. 40 | 41 | ```vim 42 | let g:NERDTreeGitStatusIndicatorMapCustom = { 43 | \ 'Modified' :'✹', 44 | \ 'Staged' :'✚', 45 | \ 'Untracked' :'✭', 46 | \ 'Renamed' :'➜', 47 | \ 'Unmerged' :'═', 48 | \ 'Deleted' :'✖', 49 | \ 'Dirty' :'✗', 50 | \ 'Ignored' :'☒', 51 | \ 'Clean' :'✔︎', 52 | \ 'Unknown' :'?', 53 | \ } 54 | ``` 55 | 56 | There is a predefined map used *nerdfonts*, to enable it 57 | 58 | ```vim 59 | let g:NERDTreeGitStatusUseNerdFonts = 1 " you should install nerdfonts by yourself. default: 0 60 | ``` 61 | 62 | > How to show `ignored` status? 63 | 64 | ```vim 65 | let g:NERDTreeGitStatusShowIgnored = 1 " a heavy feature may cost much more time. default: 0 66 | ``` 67 | 68 | > How to cooperate with [vim-devicons](https://github.com/ryanoasis/vim-devicons) 69 | 70 | ```vim 71 | Plug 'preservim/nerdtree' | 72 | \ Plug 'Xuyuanp/nerdtree-git-plugin' | 73 | \ Plug 'ryanoasis/vim-devicons' 74 | ``` 75 | 76 | Make sure they are in the right order. 77 | 78 | > How to indicate every single `untracked` file under an `untracked` dir? 79 | 80 | ```vim 81 | let g:NERDTreeGitStatusUntrackedFilesMode = 'all' " a heavy feature too. default: normal 82 | ``` 83 | 84 | > How to set `git` executable file path? 85 | 86 | ```vim 87 | let g:NERDTreeGitStatusGitBinPath = '/your/file/path' " default: git (auto find in path) 88 | ``` 89 | 90 | > How to show `Clean` indicator? 91 | 92 | ```vim 93 | let g:NERDTreeGitStatusShowClean = 1 " default: 0 94 | ``` 95 | 96 | > How to hide the boring brackets(`[ ]`)? 97 | 98 | ```vim 99 | let g:NERDTreeGitStatusConcealBrackets = 1 " default: 0 100 | ``` 101 | 102 | **NOTICE**: DO NOT enable this feature if you have also installed [vim-devicons](https://github.com/ryanoasis/vim-devicons). 103 | 104 | ## Shameless Self Promotion 105 | 106 | [Yanil](https://github.com/Xuyuanp/yanil): Yet Another Nerdtree In Lua 107 | 108 | ## Credits 109 | 110 | * [scrooloose](https://github.com/scrooloose): Open API for me. 111 | * [git\_nerd](https://github.com/swerner/git_nerd): Where my idea comes from. 112 | * [PickRelated](https://github.com/PickRelated): Add custom indicators & Review code. 113 | -------------------------------------------------------------------------------- /autoload/gitstatus/job.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " File: autoload/gitstatus/job.vim 3 | " Description: async-jobs 4 | " Maintainer: Xuyuan Pang 5 | " License: This program is free software. It comes without any warranty, 6 | " to the extent permitted by applicable law. You can redistribute 7 | " it and/or modify it under the terms of the Do What The Fuck You 8 | " Want To Public License, Version 2, as published by Sam Hocevar. 9 | " See http://sam.zoy.org/wtfpl/COPYING for more details. 10 | " ============================================================================ 11 | if exists('g:loaded_nerdtree_git_status_job') 12 | finish 13 | endif 14 | let g:loaded_nerdtree_git_status_job = 1 15 | 16 | " stolen from vim-plug 17 | let s:nvim = has('nvim-0.2') || (has('nvim') && exists('*jobwait')) 18 | let s:vim8 = has('patch-8.0.0039') && exists('*job_start') 19 | 20 | let s:Job = { 21 | \ 'running': 0, 22 | \ 'failed': 0, 23 | \ 'chunks': [''], 24 | \ 'err_chunks': [''], 25 | \ } 26 | 27 | " disabled ProhibitImplicitScopeVariable because we will use lots of `self` 28 | " disabled ProhibitUnusedVariable because lambda 29 | " vint: -ProhibitImplicitScopeVariable -ProhibitUnusedVariable 30 | function! s:newJob(name, opts) abort 31 | return extend(deepcopy(s:Job), { 32 | \ 'name': a:name, 33 | \ 'opts': a:opts 34 | \ }) 35 | endfunction 36 | 37 | function! s:Job.onStdoutCB(data) abort 38 | let self.chunks[-1] .= a:data[0] 39 | call extend(self.chunks, a:data[1:]) 40 | endfunction 41 | 42 | function! s:Job.onStderrCB(data) abort 43 | let self.failed = self.failed || !s:isEOF(a:data) 44 | let self.err_chunks[-1] .= a:data[0] 45 | call extend(self.err_chunks, a:data[1:]) 46 | endfunction 47 | 48 | function! s:Job.onExitCB() abort 49 | let self.running = 0 50 | if self.failed 51 | call self.onFailed() 52 | else 53 | call self.onSuccess() 54 | endif 55 | endfunction 56 | 57 | function! s:Job.onFailed() abort 58 | if has_key(self.opts, 'on_failed_cb') 59 | call call(self.opts.on_failed_cb, [self]) 60 | endif 61 | endfunction 62 | 63 | function! s:Job.onSuccess() abort 64 | if has_key(self.opts, 'on_success_cb') 65 | call call(self.opts.on_success_cb, [self]) 66 | endif 67 | endfunction 68 | 69 | if s:nvim 70 | function! s:Job.run(cmd) abort 71 | let jid = jobstart(a:cmd, { 72 | \ 'on_stdout': {_job_id, data, _event -> self.onStdoutCB(data)}, 73 | \ 'on_stderr': {_job_id, data, _event -> self.onStderrCB(data)}, 74 | \ 'on_exit': {_job_id, _data, _event -> self.onExitCB()}, 75 | \ 'env': {'GIT_OPTIONAL_LOCKS': '0'}, 76 | \ }) 77 | let self.id = jid 78 | let self.running = jid > 0 79 | if jid <= 0 80 | let self.failed = 1 81 | let self.err_chunks = jid == 0 ? 82 | \ ['invalid arguments'] : 83 | \ ['command is not executable'] 84 | call self.onExitCB() 85 | endif 86 | endfunction 87 | elseif s:vim8 88 | function! s:Job.run(cmd) abort 89 | let options = { 90 | \ 'out_cb': { _ch, data -> self.onStdoutCB([data]) }, 91 | \ 'err_cb': { _ch, data -> self.onStderrCB([data]) }, 92 | \ 'close_cb': { _ch -> self.onExitCB() }, 93 | \ 'out_mode': 'nl', 94 | \ 'err_mode': 'nl', 95 | \ 'env': {'GIT_OPTIONAL_LOCKS': '0'}, 96 | \ } 97 | if has('patch-8.1.350') 98 | let options['noblock'] = 1 99 | endif 100 | let jid = job_start(a:cmd, options) 101 | if job_status(jid) ==# 'run' 102 | let self.id = jid 103 | let self.running = 1 104 | else 105 | let self.running = 0 106 | let self.failed = 1 107 | let self.err_chunks = ['failed to start job'] 108 | call self.onExitCB() 109 | endif 110 | endfunction 111 | else 112 | function! s:Job.run(cmd) abort 113 | let bak = $GIT_OPTIONAL_LOCKS 114 | let $GIT_OPTIONAL_LOCKS = 0 115 | let output = substitute(system(join(a:cmd, ' ')), "\", "\n", 'g') 116 | let $GIT_OPTIONAL_LOCKS = bak 117 | let self.failed = v:shell_error isnot# 0 118 | if self.failed 119 | let self.err_chunks = [output] 120 | else 121 | let self.chunks = [output] 122 | endif 123 | call self.onExitCB() 124 | endfunction 125 | endif 126 | " vint: +ProhibitImplicitScopeVariable +ProhibitUnusedVariable 127 | 128 | function! s:isEOF(data) abort 129 | return len(a:data) == 1 && a:data[0] is# '' 130 | endfunction 131 | 132 | function! gitstatus#job#Spawn(name, cmd, opts) abort 133 | let l:job = s:newJob(a:name, a:opts) 134 | call l:job.run(a:cmd) 135 | return l:job 136 | endfunction 137 | -------------------------------------------------------------------------------- /tests/test_all.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('Test for nerdtree-git-plugin') 2 | let s:assert = themis#helper('assert') 3 | call themis#helper('command').with(s:) 4 | 5 | function! s:suite.Initializing() abort 6 | NERDTreeFocus 7 | call s:assert.exists('g:NERDTree') 8 | call s:assert.exists('g:loaded_nerdtree_git_status') 9 | call g:NERDTree.CursorToTreeWin() 10 | call s:assert.exists('b:NERDTree') 11 | endfunction 12 | 13 | function! s:suite.BuildGitWorkdirCommand() abort 14 | let l:cmd = gitstatus#util#BuildGitWorkdirCommand('/workdir', {}) 15 | call s:assert.equal(l:cmd, ['git', '-C', '/workdir', 'rev-parse', '--show-toplevel']) 16 | 17 | let l:cmd = gitstatus#util#BuildGitWorkdirCommand('/workdir', {'NERDTreeGitStatusGitBinPath': '/path/to/git'}) 18 | call s:assert.equal(l:cmd, ['/path/to/git', '-C', '/workdir', 'rev-parse', '--show-toplevel']) 19 | endfunction 20 | 21 | function! s:suite.BuildGitStatusCommand() abort 22 | let l:cmd = gitstatus#util#BuildGitStatusCommand('/workdir', {}) 23 | call s:assert.equal(l:cmd, ['git', '-C', '/workdir', 'status', '--porcelain=v2', '-z']) 24 | 25 | let l:cmd = gitstatus#util#BuildGitStatusCommand('/workdir', { 26 | \ 'NERDTreeGitStatusPorcelainVersion': 1 27 | \ }) 28 | call s:assert.equal(l:cmd, ['git', '-C', '/workdir', 'status', '--porcelain', '-z']) 29 | 30 | let l:cmd = gitstatus#util#BuildGitStatusCommand('/workdir', { 31 | \ 'NERDTreeGitStatusUntrackedFilesMode': 'all' 32 | \ }) 33 | call s:assert.equal(l:cmd, ['git', '-C', '/workdir', 'status', '--porcelain=v2', '-z', '--untracked-files=all']) 34 | 35 | let l:cmd = gitstatus#util#BuildGitStatusCommand('/workdir', { 36 | \ 'NERDTreeGitStatusShowIgnored': 1 37 | \ }) 38 | call s:assert.equal(l:cmd, ['git', '-C', '/workdir', 'status', '--porcelain=v2', '-z', '--ignored=traditional']) 39 | 40 | let l:cmd = gitstatus#util#BuildGitStatusCommand('/workdir', { 41 | \ 'NERDTreeGitStatusShowIgnored': 0 42 | \ }) 43 | call s:assert.equal(l:cmd, ['git', '-C', '/workdir', 'status', '--porcelain=v2', '-z']) 44 | 45 | let l:cmd = gitstatus#util#BuildGitStatusCommand('/workdir', { 46 | \ 'NERDTreeGitStatusIgnoreSubmodules': 'dirty' 47 | \ }) 48 | call s:assert.equal(l:cmd, ['git', '-C', '/workdir', 'status', '--porcelain=v2', '-z', '--ignore-submodules=dirty']) 49 | 50 | let l:cmd = gitstatus#util#BuildGitStatusCommand('/workdir', { 51 | \ 'NERDTreeGitStatusPorcelainVersion': 1, 52 | \ 'NERDTreeGitStatusUntrackedFilesMode': 'all', 53 | \ 'NERDTreeGitStatusShowIgnored': 1, 54 | \ 'NERDTreeGitStatusIgnoreSubmodules': 'dirty' 55 | \ }) 56 | call s:assert.equal(l:cmd, ['git', '-C', '/workdir', 'status', '--porcelain', '-z', 57 | \ '--untracked-files=all', 58 | \ '--ignored=traditional', 59 | \ '--ignore-submodules=dirty']) 60 | endfunction 61 | 62 | function! s:suite.Logger() abort 63 | let l:logger = gitstatus#log#NewLogger(1) " info 64 | let l:messages = execute('messages') 65 | 66 | call l:logger.debug('debug') 67 | call s:assert.equal(execute('messages'), l:messages) 68 | 69 | call l:logger.error('error') 70 | call s:assert.equal(execute('messages'), l:messages . "\n[nerdtree-git-status] error") 71 | endfunction 72 | 73 | function! s:suite.CustomIndicator() abort 74 | let g:NERDTreeGitStatusIndicatorMapCustom = {'Untracked': '~'} 75 | 76 | let l:staged = gitstatus#getIndicator('Staged') 77 | 78 | call s:assert.equal(gitstatus#getIndicator('Staged'), l:staged) 79 | call s:assert.equal(gitstatus#getIndicator('Untracked'), '~') 80 | 81 | " Vim(return):E716: Key not present in Dictionary 82 | Throws /E716/ gitstatus#getIndicator('no such status') 83 | endfunction 84 | 85 | function! s:suite.UpdateParentDirsStatus() abort 86 | let l:opts = {'NERDTreeGitStatusDirDirtyOnly': 1} 87 | let l:root = '/root' 88 | let l:cache = {} 89 | let l:pathStr = '/root/dir1/dir2/dir3' 90 | let l:cache[l:pathStr] = 'Untracked' 91 | call gitstatus#util#UpdateParentDirsStatus(l:cache, l:root, l:pathStr, 'Untracked', l:opts) 92 | call s:assert.equal({'/root/dir1': 'Dirty', '/root/dir1/dir2': 'Dirty', '/root/dir1/dir2/dir3': 'Untracked'}, l:cache) 93 | 94 | let l:pathStr = '/root/dir1/dir2/file0' 95 | let l:cache[l:pathStr] = 'Staged' 96 | call gitstatus#util#UpdateParentDirsStatus(l:cache, l:root, l:pathStr, 'Staged', l:opts) 97 | call s:assert.equal({'/root/dir1': 'Dirty', '/root/dir1/dir2': 'Dirty', '/root/dir1/dir2/dir3': 'Untracked', '/root/dir1/dir2/file0': 'Staged'}, l:cache) 98 | 99 | let l:opts = {'NERDTreeGitStatusDirDirtyOnly': 0} 100 | let l:cache = {} 101 | let l:pathStr = '/root/dir1/dir2/dir3' 102 | let l:cache[l:pathStr] = 'Untracked' 103 | call gitstatus#util#UpdateParentDirsStatus(l:cache, l:root, l:pathStr, 'Untracked', l:opts) 104 | call s:assert.equal({'/root/dir1': 'Untracked', '/root/dir1/dir2': 'Untracked', '/root/dir1/dir2/dir3': 'Untracked'}, l:cache) 105 | 106 | let l:cache['/root/dir1/file1'] = 'Staged' 107 | call gitstatus#util#UpdateParentDirsStatus(l:cache, l:root, '/root/dir1/file1', 'Staged', l:opts) 108 | call s:assert.equal({'/root/dir1': 'Dirty','/root/dir1/file1': 'Staged', '/root/dir1/dir2': 'Untracked', '/root/dir1/dir2/dir3': 'Untracked'}, l:cache) 109 | endfunction 110 | -------------------------------------------------------------------------------- /autoload/gitstatus/doctor.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " File: autoload/gitstatus/doctor.vim 3 | " Description: what does the doctor say? 4 | " Maintainer: Xuyuan Pang 5 | " License: This program is free software. It comes without any warranty, 6 | " to the extent permitted by applicable law. You can redistribute 7 | " it and/or modify it under the terms of the Do What The Fuck You 8 | " Want To Public License, Version 2, as published by Sam Hocevar. 9 | " See http://sam.zoy.org/wtfpl/COPYING for more details. 10 | " ============================================================================ 11 | 12 | let s:types = { 13 | \ 'NUMBER': type(0), 14 | \ 'STRING': type(''), 15 | \ 'FUNCREF': type(function('tr')), 16 | \ 'LIST': type([]), 17 | \ 'DICT': type({}), 18 | \ 'FLOAT': type(0.0), 19 | \ 'BOOL': type(v:true), 20 | \ 'NULL': type(v:null) 21 | \ } 22 | 23 | let s:type_formatters = {} 24 | let s:type_formatters[s:types.NUMBER] = { nbr -> string(nbr) } 25 | let s:type_formatters[s:types.STRING] = { str -> printf("'%s'", str) } 26 | let s:type_formatters[s:types.FUNCREF] = { fn -> string(fn) } 27 | let s:type_formatters[s:types.LIST] = { lst -> s:prettifyList(lst, ' \ ', 0, ' ') } 28 | let s:type_formatters[s:types.DICT] = { dct -> s:prettifyDict(dct, ' \ ', 0, ' ') } 29 | let s:type_formatters[s:types.FLOAT] = { flt -> string(flt) } 30 | let s:type_formatters[s:types.BOOL] = { bol -> bol ? 'v:true' : 'v:false' } 31 | let s:type_formatters[s:types.NULL] = { nul -> string(nul) } 32 | 33 | function! s:get_git_version() abort 34 | return split(system('git version'), "\n")[0] 35 | endfunction 36 | 37 | function! s:get_git_status_output(workdir) abort 38 | return system(join(gitstatus#util#BuildGitStatusCommand(a:workdir, g:), ' ')) 39 | endfunction 40 | 41 | function! s:prettifyDict(obj, prefix, level, indent) abort 42 | let l:prefix = a:prefix . repeat(a:indent, a:level) 43 | if empty(a:obj) 44 | return '{}' 45 | endif 46 | let l:res = "{\n" 47 | for [l:key, l:val] in items(a:obj) 48 | let l:type = type(l:val) 49 | if l:type is# s:types.DICT 50 | let l:val = s:prettifyDict(l:val, a:prefix, a:level + 1, a:indent) 51 | elseif l:type is# s:types.LIST 52 | let l:val = s:prettifyList(l:val, a:prefix , a:level + 1, a:indent) 53 | else 54 | let l:val = s:prettify(l:val) 55 | endif 56 | let l:res .= l:prefix . a:indent . "'" . l:key . "': " . l:val . ",\n" 57 | endfor 58 | let l:res .= l:prefix . '}' 59 | return l:res 60 | endfunction 61 | 62 | function! s:prettifyList(obj, prefix, level, indent) abort 63 | let l:prefix = a:prefix . repeat(a:indent, a:level) 64 | if empty(a:obj) 65 | return '[]' 66 | endif 67 | let l:res = "[\n" 68 | for l:val in a:obj 69 | let l:type = type(l:val) 70 | if l:type is# s:types.LIST 71 | let l:val = s:prettifyList(l:val, a:prefix, a:level + 1, a:indent) 72 | elseif l:type is# s:types.DICT 73 | let l:val = s:prettifyDict(l:val, a:prefix, a:level + 1, a:indent) 74 | else 75 | let l:val = s:prettify(l:val) 76 | endif 77 | let l:res .= l:prefix . a:indent . l:val . ",\n" 78 | endfor 79 | let l:res .= l:prefix . ']' 80 | return l:res 81 | endfunction 82 | 83 | function! s:prettify(obj) abort 84 | let l:type = type(a:obj) 85 | return call(s:type_formatters[l:type], [a:obj]) 86 | endfunction 87 | 88 | function! s:loaded_vim_devicons() abort 89 | return get(g:, 'loaded_webdevicons', 0) && get(g:, 'webdevicons_enable', 0) && get(g:, 'webdevicons_enable_nerdtree', 0) 90 | endfunction 91 | 92 | function! s:loaded_vim_nerdtree_syntax_highlight() abort 93 | return exists('g:NERDTreeSyntaxEnabledExtensions') 94 | endfunction 95 | 96 | function! s:loaded_vim_nerdtree_tabs() abort 97 | return exists('g:nerdtree_tabs_open_on_gui_startup') 98 | endfunction 99 | 100 | function! gitstatus#doctor#Say() abort 101 | call g:NERDTree.MustBeOpen() 102 | call g:NERDTree.CursorToTreeWin() 103 | 104 | let l:line = repeat('=', 80) 105 | 106 | echo has('nvim') ? 'Neovim:' : 'Vim:' 107 | echo execute('version') 108 | echo l:line 109 | 110 | echo 'NERDTree:' 111 | echo 'version: ' . nerdtree#version() 112 | echo 'root: ' . b:NERDTree.root.path.str() 113 | 114 | echo l:line 115 | 116 | echo 'Git:' 117 | echo 'version: ' . s:get_git_version() 118 | let l:git_workdir = get(g:, 'NTGitWorkdir', '') 119 | echo 'workdir: ' . l:git_workdir 120 | if !empty(l:git_workdir) 121 | echo 'status output:' 122 | echo s:get_git_status_output(l:git_workdir) 123 | endif 124 | 125 | echo l:line 126 | 127 | echo 'Options:' 128 | for [l:key, l:val] in items(g:) 129 | if l:key =~# 'NERDTreeGitStatus*' 130 | echo '' . l:key . ' = ' . s:prettify(l:val) 131 | endif 132 | endfor 133 | 134 | echo l:line 135 | 136 | echo 'Others:' 137 | echo 'vim-devicons: ' . (s:loaded_vim_devicons() ? 'yes' : 'no') 138 | if s:loaded_vim_devicons() 139 | for [l:key, l:val] in items(g:) 140 | if l:key =~# 'WebDevIconsNerdTree*' 141 | echo '' . l:key . ' = ' . s:prettify(l:val) 142 | endif 143 | endfor 144 | endif 145 | 146 | echo repeat('-', 40) 147 | echo 'vim-nerdtree-syntax-highlight: ' . (s:loaded_vim_nerdtree_syntax_highlight() ? 'yes': 'no') 148 | if s:loaded_vim_nerdtree_syntax_highlight() 149 | for [l:key, l:val] in items(g:) 150 | if l:key =~# 'NERDTreeSyntax*' 151 | echo '' . l:key . ' = ' . s:prettify(l:val) 152 | endif 153 | endfor 154 | endif 155 | 156 | echo repeat('-', 40) 157 | echo 'vim-nerdtree-tabs: ' . (s:loaded_vim_nerdtree_tabs() ? 'yes': 'no') 158 | if s:loaded_vim_nerdtree_tabs() 159 | for [l:key, l:val] in items(g:) 160 | if l:key =~# 'nerdtree_tabs_*' 161 | echo '' . l:key . ' = ' . s:prettify(l:val) 162 | endif 163 | endfor 164 | endif 165 | 166 | echo l:line 167 | endfunction 168 | -------------------------------------------------------------------------------- /autoload/gitstatus/util.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " File: autoload/git_status/util.vim 3 | " Description: utils 4 | " Maintainer: Xuyuan Pang 5 | " License: This program is free software. It comes without any warranty, 6 | " to the extent permitted by applicable law. You can redistribute 7 | " it and/or modify it under the terms of the Do What The Fuck You 8 | " Want To Public License, Version 2, as published by Sam Hocevar. 9 | " See http://sam.zoy.org/wtfpl/COPYING for more details. 10 | " ============================================================================ 11 | if exists('g:loaded_nerdtree_git_status_util') 12 | finish 13 | endif 14 | let g:loaded_nerdtree_git_status_util = 1 15 | 16 | " FUNCTION: gitstatus#utilFormatPath 17 | " This function is used to format nerdtree.Path. 18 | " For Windows, returns in format 'C:/path/to/file' 19 | " 20 | " ARGS: 21 | " path: nerdtree.Path 22 | " 23 | " RETURNS: 24 | " absolute path 25 | if gitstatus#isWin() 26 | if exists('+shellslash') 27 | function! gitstatus#util#FormatPath(path) abort 28 | let l:sslbak = &shellslash 29 | try 30 | set shellslash 31 | return a:path.str() 32 | finally 33 | let &shellslash = l:sslbak 34 | endtry 35 | endfunction 36 | else 37 | function! gitstatus#util#FormatPath(path) abort 38 | let l:pathStr = a:path.str() 39 | let l:pathStr = a:path.WinToUnixPath(l:pathStr) 40 | let l:pathStr = a:path.drive . l:pathStr 41 | return l:pathStr 42 | endfunction 43 | endif 44 | else 45 | function! gitstatus#util#FormatPath(path) abort 46 | return a:path.str() 47 | endfunction 48 | endif 49 | 50 | function! gitstatus#util#BuildGitWorkdirCommand(root, opts) abort 51 | return [ 52 | \ get(a:opts, 'NERDTreeGitStatusGitBinPath', 'git'), 53 | \ '-C', a:root, 54 | \ 'rev-parse', 55 | \ '--show-toplevel', 56 | \ ] 57 | endfunction 58 | 59 | function! gitstatus#util#BuildGitStatusCommand(root, opts) abort 60 | let l:cmd = [ 61 | \ get(a:opts, 'NERDTreeGitStatusGitBinPath', 'git'), 62 | \ '-C', a:root, 63 | \ 'status', 64 | \ '--porcelain' . (get(a:opts, 'NERDTreeGitStatusPorcelainVersion', 2) ==# 2 ? '=v2' : ''), 65 | \ '-z' 66 | \ ] 67 | if has_key(a:opts, 'NERDTreeGitStatusUntrackedFilesMode') 68 | let l:cmd += ['--untracked-files=' . a:opts['NERDTreeGitStatusUntrackedFilesMode']] 69 | endif 70 | 71 | if get(a:opts, 'NERDTreeGitStatusShowIgnored', 0) 72 | let l:cmd += ['--ignored=traditional'] 73 | endif 74 | 75 | if has_key(a:opts, 'NERDTreeGitStatusIgnoreSubmodules') 76 | let l:cmd += ['--ignore-submodules=' . a:opts['NERDTreeGitStatusIgnoreSubmodules']] 77 | endif 78 | 79 | if has_key(a:opts, 'NERDTreeGitStatusCwdOnly') 80 | let l:cmd += ['.'] 81 | endif 82 | 83 | return l:cmd 84 | endfunction 85 | 86 | function! gitstatus#util#ParseGitStatusLines(root, statusLines, opts) abort 87 | let l:result = {} 88 | let l:is_rename = 0 89 | for l:line in a:statusLines 90 | if l:is_rename 91 | call gitstatus#util#UpdateParentDirsStatus(l:result, a:root, a:root . '/' . l:line, 'Dirty', a:opts) 92 | let l:is_rename = 0 93 | continue 94 | endif 95 | let [l:pathStr, l:statusKey] = gitstatus#util#ParseGitStatusLine(l:line, a:opts) 96 | 97 | let l:pathStr = a:root . '/' . l:pathStr 98 | if l:pathStr[-1:-1] is# '/' 99 | let l:pathStr = l:pathStr[:-2] 100 | endif 101 | let l:is_rename = l:statusKey is# 'Renamed' 102 | let l:result[l:pathStr] = l:statusKey 103 | 104 | call gitstatus#util#UpdateParentDirsStatus(l:result, a:root, l:pathStr, l:statusKey, a:opts) 105 | endfor 106 | return l:result 107 | endfunction 108 | 109 | let s:unmerged_status = { 110 | \ 'DD': 1, 111 | \ 'AU': 1, 112 | \ 'UD': 1, 113 | \ 'UA': 1, 114 | \ 'DU': 1, 115 | \ 'AA': 1, 116 | \ 'UU': 1, 117 | \ } 118 | 119 | " Function: s:getStatusKey() function {{{2 120 | " This function is used to get git status key 121 | " 122 | " Args: 123 | " x: index tree 124 | " y: work tree 125 | " 126 | "Returns: 127 | " status key 128 | " 129 | " man git-status 130 | " X Y Meaning 131 | " ------------------------------------------------- 132 | " [MD] not updated 133 | " M [ MD] updated in index 134 | " A [ MD] added to index 135 | " D [ M] deleted from index 136 | " R [ MD] renamed in index 137 | " C [ MD] copied in index 138 | " [MARC] index and work tree matches 139 | " [ MARC] M work tree changed since index 140 | " [ MARC] D deleted in work tree 141 | " ------------------------------------------------- 142 | " D D unmerged, both deleted 143 | " A U unmerged, added by us 144 | " U D unmerged, deleted by them 145 | " U A unmerged, added by them 146 | " D U unmerged, deleted by us 147 | " A A unmerged, both added 148 | " U U unmerged, both modified 149 | " ------------------------------------------------- 150 | " ? ? untracked 151 | " ! ! ignored 152 | " ------------------------------------------------- 153 | function! s:getStatusKey(x, y) abort 154 | let l:xy = a:x . a:y 155 | if get(s:unmerged_status, l:xy, 0) 156 | return 'Unmerged' 157 | elseif l:xy ==# '??' 158 | return 'Untracked' 159 | elseif l:xy ==# '!!' 160 | return 'Ignored' 161 | elseif a:y ==# 'M' 162 | return 'Modified' 163 | elseif a:y ==# 'D' 164 | return 'Deleted' 165 | elseif a:y =~# '[RC]' 166 | return 'Renamed' 167 | elseif a:x ==# 'D' 168 | return 'Deleted' 169 | elseif a:x =~# '[MA]' 170 | return 'Staged' 171 | elseif a:x =~# '[RC]' 172 | return 'Renamed' 173 | else 174 | return 'Unknown' 175 | endif 176 | endfunction 177 | 178 | function! gitstatus#util#ParseGitStatusLine(statusLine, opts) abort 179 | if get(a:opts, 'NERDTreeGitStatusPorcelainVersion', 2) ==# 2 180 | if a:statusLine[0] ==# '1' 181 | let l:statusKey = s:getStatusKey(a:statusLine[2], a:statusLine[3]) 182 | let l:pathStr = a:statusLine[113:] 183 | elseif a:statusLine[0] ==# '2' 184 | let l:statusKey = 'Renamed' 185 | let l:pathStr = a:statusLine[113:] 186 | let l:pathStr = l:pathStr[stridx(l:pathStr, ' ')+1:] 187 | elseif a:statusLine[0] ==# 'u' 188 | let l:statusKey = 'Unmerged' 189 | let l:pathStr = a:statusLine[161:] 190 | elseif a:statusLine[0] ==# '?' 191 | let l:statusKey = 'Untracked' 192 | let l:pathStr = a:statusLine[2:] 193 | elseif a:statusLine[0] ==# '!' 194 | let l:statusKey = 'Ignored' 195 | let l:pathStr = a:statusLine[2:] 196 | else 197 | throw '[nerdtree_git_status] unknown status: ' . a:statusLine 198 | endif 199 | return [l:pathStr, l:statusKey] 200 | else 201 | let l:pathStr = a:statusLine[3:] 202 | let l:statusKey = s:getStatusKey(a:statusLine[0], a:statusLine[1]) 203 | return [l:pathStr, l:statusKey] 204 | endif 205 | endfunction 206 | 207 | function! gitstatus#util#UpdateParentDirsStatus(cache, root, pathStr, statusKey, opts) abort 208 | if get(a:cache, a:pathStr, '') ==# 'Ignored' 209 | return 210 | endif 211 | let l:dirtyPath = fnamemodify(a:pathStr, ':h') 212 | let l:dir_dirty_only = get(a:opts, 'NERDTreeGitStatusDirDirtyOnly', 1) 213 | while l:dirtyPath !=# a:root 214 | let l:key = get(a:cache, l:dirtyPath, '') 215 | if l:dir_dirty_only 216 | if l:key ==# '' 217 | let a:cache[l:dirtyPath] = 'Dirty' 218 | else 219 | return 220 | endif 221 | else 222 | if l:key ==# '' 223 | let a:cache[l:dirtyPath] = a:statusKey 224 | elseif l:key ==# 'Dirty' || l:key ==# a:statusKey 225 | return 226 | else 227 | let a:cache[l:dirtyPath] = 'Dirty' 228 | endif 229 | endif 230 | let l:dirtyPath = fnamemodify(l:dirtyPath, ':h') 231 | endwhile 232 | endfunction 233 | -------------------------------------------------------------------------------- /nerdtree_plugin/git_status.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " File: git_status.vim 3 | " Description: plugin for NERD Tree that provides git status support 4 | " Maintainer: Xuyuan Pang 5 | " License: This program is free software. It comes without any warranty, 6 | " to the extent permitted by applicable law. You can redistribute 7 | " it and/or modify it under the terms of the Do What The Fuck You 8 | " Want To Public License, Version 2, as published by Sam Hocevar. 9 | " See http://sam.zoy.org/wtfpl/COPYING for more details. 10 | " ============================================================================ 11 | scriptencoding utf-8 12 | 13 | if exists('g:loaded_nerdtree_git_status') 14 | finish 15 | endif 16 | let g:loaded_nerdtree_git_status = 1 17 | 18 | let s:is_win = gitstatus#isWin() 19 | 20 | " stolen from nerdtree 21 | "Function: s:initVariable() function {{{2 22 | "This function is used to initialise a given variable to a given value. The 23 | "variable is only initialised if it does not exist prior 24 | " 25 | "Args: 26 | "var: the name of the var to be initialised 27 | "value: the value to initialise var to 28 | " 29 | "Returns: 30 | "1 if the var is set, 0 otherwise 31 | function! s:initVariable(var, value) abort 32 | if !exists(a:var) 33 | exec 'let ' . a:var . ' = ' . "'" . substitute(a:value, "'", "''", 'g') . "'" 34 | return 1 35 | endif 36 | return 0 37 | endfunction 38 | 39 | let s:default_vals = { 40 | \ 'g:NERDTreeGitStatusEnable': 1, 41 | \ 'g:NERDTreeGitStatusUpdateOnWrite': 1, 42 | \ 'g:NERDTreeGitStatusUpdateOnCursorHold': 1, 43 | \ 'g:NERDTreeGitStatusShowIgnored': 0, 44 | \ 'g:NERDTreeGitStatusUseNerdFonts': 0, 45 | \ 'g:NERDTreeGitStatusDirDirtyOnly': 1, 46 | \ 'g:NERDTreeGitStatusConcealBrackets': 0, 47 | \ 'g:NERDTreeGitStatusAlignIfConceal': 1, 48 | \ 'g:NERDTreeGitStatusShowClean': 0, 49 | \ 'g:NERDTreeGitStatusLogLevel': 2, 50 | \ 'g:NERDTreeGitStatusPorcelainVersion': 2, 51 | \ 'g:NERDTreeGitStatusCwdOnly': 0, 52 | \ 'g:NERDTreeGitStatusMapNextHunk': ']c', 53 | \ 'g:NERDTreeGitStatusMapPrevHunk': '[c', 54 | \ 'g:NERDTreeGitStatusUntrackedFilesMode': 'normal', 55 | \ 'g:NERDTreeGitStatusGitBinPath': 'git', 56 | \ } 57 | 58 | for [s:var, s:value] in items(s:default_vals) 59 | call s:initVariable(s:var, s:value) 60 | endfor 61 | 62 | let s:logger = gitstatus#log#NewLogger(g:NERDTreeGitStatusLogLevel) 63 | 64 | function! s:deprecated(oldv, newv) abort 65 | call s:logger.warning(printf("option '%s' is deprecated, please use '%s'", a:oldv, a:newv)) 66 | endfunction 67 | 68 | function! s:migrateVariable(oldv, newv) abort 69 | if exists(a:oldv) 70 | call s:deprecated(a:oldv, a:newv) 71 | exec 'let ' . a:newv . ' = ' . a:oldv 72 | return 1 73 | endif 74 | return 0 75 | endfunction 76 | 77 | let s:need_migrate_vals = { 78 | \ 'g:NERDTreeShowGitStatus': 'g:NERDTreeGitStatusEnable', 79 | \ 'g:NERDTreeUpdateOnWrite': 'g:NERDTreeGitStatusUpdateOnWrite', 80 | \ 'g:NERDTreeMapNextHunk': 'g:NERDTreeGitStatusMapNextHunk', 81 | \ 'g:NERDTreeMapPrevHunk': 'g:NERDTreeGitStatusMapPrevHunk', 82 | \ 'g:NERDTreeShowIgnoredStatus': 'g:NERDTreeGitStatusShowIgnored', 83 | \ 'g:NERDTreeIndicatorMapCustom': 'g:NERDTreeGitStatusIndicatorMapCustom', 84 | \ } 85 | 86 | for [s:oldv, s:newv] in items(s:need_migrate_vals) 87 | call s:migrateVariable(s:oldv, s:newv) 88 | endfor 89 | 90 | if !g:NERDTreeGitStatusEnable 91 | finish 92 | endif 93 | 94 | if !executable(g:NERDTreeGitStatusGitBinPath) 95 | call s:logger.error('git command not found') 96 | finish 97 | endif 98 | 99 | " FUNCTION: path2str 100 | " This function is used to format nerdtree.Path. 101 | " For Windows, returns in format 'C:/path/to/file' 102 | " 103 | " ARGS: 104 | " path: nerdtree.Path 105 | " 106 | " RETURNS: 107 | " absolute path 108 | function! s:path2str(path) abort 109 | return gitstatus#util#FormatPath(a:path) 110 | endfunction 111 | 112 | " disable ProhibitUnusedVariable because these three functions used to callback 113 | " vint: -ProhibitUnusedVariable 114 | function! s:onGitWorkdirSuccessCB(job) abort 115 | let g:NTGitWorkdir = split(join(a:job.chunks, ''), "\n")[0] 116 | call s:logger.debug(printf("'%s' is in a git repo: '%s'", a:job.opts.cwd, g:NTGitWorkdir)) 117 | call s:enableLiveUpdate() 118 | 119 | call s:refreshGitStatus('init', g:NTGitWorkdir) 120 | endfunction 121 | 122 | function! s:onGitWorkdirFailedCB(job) abort 123 | let l:errormsg = join(a:job.err_chunks, '') 124 | if l:errormsg =~# 'fatal: Not a git repository' 125 | call s:logger.debug(printf("'%s' is not in a git repo", a:job.opts.cwd)) 126 | endif 127 | call s:disableLiveUpdate() 128 | unlet! g:NTGitWorkdir 129 | endfunction 130 | 131 | function! s:getGitWorkdir(ntRoot) abort 132 | call gitstatus#job#Spawn('git-workdir', 133 | \ s:buildGitWorkdirCommand(a:ntRoot), 134 | \ { 135 | \ 'on_success_cb': function('s:onGitWorkdirSuccessCB'), 136 | \ 'on_failed_cb': function('s:onGitWorkdirFailedCB'), 137 | \ 'cwd': a:ntRoot, 138 | \ }) 139 | endfunction 140 | " vint: +ProhibitUnusedVariable 141 | 142 | function! s:buildGitWorkdirCommand(root) abort 143 | return gitstatus#util#BuildGitWorkdirCommand(a:root, g:) 144 | endfunction 145 | 146 | function! s:buildGitStatusCommand(workdir) abort 147 | return gitstatus#util#BuildGitStatusCommand(a:workdir, g:) 148 | endfunction 149 | 150 | function! s:refreshGitStatus(name, workdir) abort 151 | let l:opts = { 152 | \ 'on_failed_cb': function('s:onGitStatusFailedCB'), 153 | \ 'on_success_cb': function('s:onGitStatusSuccessCB'), 154 | \ 'cwd': a:workdir 155 | \ } 156 | let l:job = gitstatus#job#Spawn(a:name, s:buildGitStatusCommand(a:workdir), l:opts) 157 | return l:job 158 | endfunction 159 | 160 | " vint: -ProhibitUnusedVariable 161 | function! s:onGitStatusSuccessCB(job) abort 162 | if !exists('g:NTGitWorkdir') || g:NTGitWorkdir !=# a:job.opts.cwd 163 | call s:logger.debug(printf("git workdir has changed: '%s' -> '%s'", a:job.opts.cwd, get(g:, 'NTGitWorkdir', ''))) 164 | return 165 | endif 166 | let l:output = join(a:job.chunks, '') 167 | let l:lines = split(l:output, "\n") 168 | let l:cache = gitstatus#util#ParseGitStatusLines(a:job.opts.cwd, l:lines, g:) 169 | 170 | try 171 | call s:listener.SetNext(l:cache) 172 | call s:listener.TryUpdateNERDTreeUI() 173 | catch 174 | endtry 175 | endfunction 176 | 177 | function! s:onGitStatusFailedCB(job) abort 178 | let l:errormsg = join(a:job.err_chunks, '') 179 | if l:errormsg =~# "error: option `porcelain' takes no value" 180 | 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")) 181 | call s:disableLiveUpdate() 182 | unlet! g:NTGitWorkdir 183 | elseif l:errormsg =~# '^warning: could not open .* Permission denied' 184 | call s:onGitStatusSuccessCB(a:job) 185 | else 186 | call s:logger.error(printf('job[%s] failed: %s', a:job.name, l:errormsg)) 187 | endif 188 | endfunction 189 | 190 | " FUNCTION: s:onCursorHold(fname) {{{2 191 | function! s:onCursorHold(fname) 192 | " Do not update when a special buffer is selected 193 | if !empty(&l:buftype) 194 | return 195 | endif 196 | let l:fname = s:is_win ? 197 | \ substitute(a:fname, '\', '/', 'g') : 198 | \ a:fname 199 | 200 | if !exists('g:NTGitWorkdir') || !s:hasPrefix(l:fname, g:NTGitWorkdir) 201 | return 202 | endif 203 | 204 | let l:job = s:refreshGitStatus('cursor-hold', g:NTGitWorkdir) 205 | call s:logger.debug('run cursor-hold job: ' . l:job.id) 206 | endfunction 207 | 208 | " FUNCTION: s:onFileUpdate(fname) {{{2 209 | function! s:onFileUpdate(fname) 210 | let l:fname = s:is_win ? 211 | \ substitute(a:fname, '\', '/', 'g') : 212 | \ a:fname 213 | if !exists('g:NTGitWorkdir') || !s:hasPrefix(l:fname, g:NTGitWorkdir) 214 | return 215 | endif 216 | let l:job = s:refreshGitStatus('file-update', g:NTGitWorkdir) 217 | call s:logger.debug('run file-update job: ' . l:job.id) 218 | endfunction 219 | " vint: +ProhibitUnusedVariable 220 | 221 | function! s:hasPrefix(text, prefix) abort 222 | return len(a:text) >= len(a:prefix) && a:text[:len(a:prefix)-1] ==# a:prefix 223 | endfunction 224 | 225 | function! s:setupNERDTreeListeners(listener) abort 226 | call g:NERDTreePathNotifier.AddListener('init', a:listener.OnInit) 227 | call g:NERDTreePathNotifier.AddListener('refresh', a:listener.OnRefresh) 228 | call g:NERDTreePathNotifier.AddListener('refreshFlags', a:listener.OnRefreshFlags) 229 | endfunction 230 | 231 | " FUNCTION: s:findHunk(node, direction) 232 | " Args: 233 | " node: the current node 234 | " direction: next(>0) or prev(<0) 235 | " 236 | " Returns: 237 | " lineNum if the hunk found, -1 otherwise 238 | function! s:findHunk(node, direction) abort 239 | let l:ui = b:NERDTree.ui 240 | let l:rootLn = l:ui.getRootLineNum() 241 | let l:totalLn = line('$') 242 | let l:currLn = l:ui.getLineNum(a:node) 243 | let l:currLn = l:currLn <= l:rootLn ? l:rootLn+1 : l:currLn 244 | let l:step = a:direction > 0 ? 1 : -1 245 | let l:lines = a:direction > 0 ? 246 | \ range(l:currLn+1, l:totalLn, l:step) + range(l:rootLn+1, l:currLn-1, l:step) : 247 | \ range(l:currLn-1, l:rootLn+1, l:step) + range(l:totalLn, l:currLn+1, l:step) 248 | for l:ln in l:lines 249 | let l:path = s:path2str(l:ui.getPath(l:ln)) 250 | if s:listener.HasPath(l:path) 251 | return l:ln 252 | endif 253 | endfor 254 | return -1 255 | endfunction 256 | 257 | " vint: -ProhibitUnusedVariable 258 | " FUNCTION: s:jumpToNextHunk(node) {{{2 259 | function! s:jumpToNextHunk(node) 260 | let l:ln = s:findHunk(a:node, 1) 261 | if l:ln > 0 262 | exec '' . l:ln 263 | call s:logger.info('Jump to next hunk') 264 | endif 265 | endfunction 266 | 267 | " FUNCTION: s:jumpToPrevHunk(node) {{{2 268 | function! s:jumpToPrevHunk(node) 269 | let l:ln = s:findHunk(a:node, -1) 270 | if l:ln > 0 271 | exec '' . l:ln 272 | call s:logger.info('Jump to prev hunk') 273 | endif 274 | endfunction 275 | " vint: +ProhibitUnusedVariable 276 | 277 | " Function: s:SID() {{{2 278 | function s:SID() 279 | if !exists('s:sid') 280 | let s:sid = matchstr(expand(''), '\zs\d\+\ze_SID$') 281 | endif 282 | return s:sid 283 | endfun 284 | 285 | " FUNCTION: s:setupNERDTreeKeyMappings {{{2 286 | function! s:setupNERDTreeKeyMappings() 287 | let l:s = '' . s:SID() . '_' 288 | 289 | call NERDTreeAddKeyMap({ 290 | \ 'key': g:NERDTreeGitStatusMapNextHunk, 291 | \ 'scope': 'Node', 292 | \ 'callback': l:s.'jumpToNextHunk', 293 | \ 'quickhelpText': 'Jump to next git hunk' }) 294 | 295 | call NERDTreeAddKeyMap({ 296 | \ 'key': g:NERDTreeGitStatusMapPrevHunk, 297 | \ 'scope': 'Node', 298 | \ 'callback': l:s.'jumpToPrevHunk', 299 | \ 'quickhelpText': 'Jump to prev git hunk' }) 300 | endfunction 301 | 302 | 303 | " I don't know why, but vint said they are unused. 304 | " vint: -ProhibitUnusedVariable 305 | function! s:onNERDTreeDirChanged(path) abort 306 | call s:getGitWorkdir(a:path) 307 | endfunction 308 | 309 | function! s:onNERDTreeInit(path) abort 310 | call s:getGitWorkdir(a:path) 311 | endfunction 312 | " vint: +ProhibitUnusedVariable 313 | 314 | function! s:enableLiveUpdate() abort 315 | augroup nerdtreegitplugin_liveupdate 316 | autocmd! 317 | if g:NERDTreeGitStatusUpdateOnWrite 318 | autocmd BufWritePost * silent! call s:onFileUpdate(expand('%:p')) 319 | endif 320 | 321 | if g:NERDTreeGitStatusUpdateOnCursorHold 322 | autocmd CursorHold * silent! call s:onCursorHold(expand('%:p')) 323 | endif 324 | 325 | " TODO: is it necessary to pass the buffer name? 326 | autocmd User FugitiveChanged silent! call s:onFileUpdate(expand('%:p')) 327 | 328 | autocmd BufEnter NERD_tree_* if exists('b:NERDTree') | 329 | \ call s:onNERDTreeInit(s:path2str(b:NERDTree.root.path)) | endif 330 | augroup end 331 | endfunction 332 | 333 | function! s:disableLiveUpdate() abort 334 | augroup nerdtreegitplugin_liveupdate 335 | autocmd! 336 | augroup end 337 | endfunction 338 | 339 | augroup nerdtreegitplugin 340 | autocmd! 341 | autocmd User NERDTreeInit call s:onNERDTreeInit(s:path2str(b:NERDTree.root.path)) 342 | autocmd User NERDTreeNewRoot call s:onNERDTreeDirChanged(s:path2str(b:NERDTree.root.path)) 343 | augroup end 344 | 345 | call s:setupNERDTreeKeyMappings() 346 | 347 | let s:listener = gitstatus#listener#New(g:) 348 | call s:setupNERDTreeListeners(s:listener) 349 | --------------------------------------------------------------------------------